+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/n8n/README.md b/n8n/README.md
new file mode 100644
index 0000000..aa3b32a
--- /dev/null
+++ b/n8n/README.md
@@ -0,0 +1,156 @@
+# n8n 경제 일정 워크플로우 설정 가이드
+
+## 워크플로우 구성
+
+두 개의 워크플로우로 구성됩니다:
+
+1. **Economic Calendar Collector** - 주기적으로 실제 데이터 수집
+2. **Economic Calendar API** - Webhook으로 데이터 제공
+
+```
+[Schedule: 6시간마다] → [HTTP: Forex Factory] → [Code: 변환] → [File: 저장]
+
+[Webhook 요청] → [File: 읽기] → [Code: 파싱] → [Response: JSON]
+```
+
+---
+
+## 설정 방법
+
+### 1. 데이터 수집 워크플로우 설정
+
+1. n8n 웹 UI 접속 (예: `https://n8n.yourdomain.com`)
+2. **Workflows** → **Import from File**
+3. `economic-calendar-collector.json` 파일 선택
+4. **Save** 클릭
+5. **Active** 토글을 켜서 워크플로우 활성화
+6. 수동으로 한번 실행하여 초기 데이터 생성 (▶ 버튼 클릭)
+
+### 2. API 워크플로우 설정
+
+1. **Workflows** → **Import from File**
+2. `economic-calendar-api.json` 파일 선택
+3. **Save** 클릭
+4. **Active** 토글을 켜서 워크플로우 활성화
+
+### 3. Webhook URL 확인
+
+API 워크플로우 활성화 후, Webhook 노드를 클릭하면 Production URL이 표시됩니다:
+
+```
+https://n8n.yourdomain.com/webhook/economic-calendar
+```
+
+### 4. 프론트엔드 설정 수정
+
+`app.js`에서 `dataUrl`을 n8n webhook URL로 변경:
+
+```javascript
+this.dataUrl = 'https://n8n.yourdomain.com/webhook/economic-calendar';
+```
+
+---
+
+## 데이터 소스
+
+현재 **Investing.com**의 경제 캘린더 API를 사용합니다:
+- URL: `https://sslecal2.investing.com/economic-calendar/Service/getCalendarFilteredData`
+- 제공 데이터: 향후 30일간 경제 일정
+- 필터 국가: US(미국), JP(일본), KR(한국), CN(중국)
+- 필터 중요도: 2성, 3성 이벤트만
+- 시간대: KST (한국 표준시, timeZone=88)
+
+### 데이터 수집 주기
+
+기본값: **매일 새벽 3시 (KST)** 자동 수집
+
+Cron 표현식: `0 3 * * *`
+
+변경하려면 Collector 워크플로우의 Schedule Trigger 설정 수정:
+- 매일 새벽 3시: `0 3 * * *`
+- 매일 자정: `0 0 * * *`
+- 6시간마다: `field: "hours"`, `hoursInterval: 6`
+
+---
+
+## 파일 저장 경로
+
+n8n Docker 컨테이너 내부: `/data/economic-calendar.json`
+
+Docker Compose 볼륨 매핑 필요:
+```yaml
+volumes:
+ - n8n_data:/home/node/.n8n
+ - ./data:/data # 추가
+```
+
+---
+
+## 대체 워크플로우 (수동 관리용)
+
+실제 API 대신 수동으로 데이터를 관리하려면 기존 `economic-calendar-workflow.json` 사용:
+
+1. `economic-calendar-workflow.json` import
+2. Code 노드에서 직접 이벤트 데이터 수정
+3. 활성화
+
+---
+
+## 일정 데이터 형식
+
+```json
+{
+ "date": "2025-01-15", // YYYY-MM-DD 형식
+ "time": "04:00", // HH:MM 형식 (한국시간)
+ "country": "US", // US, KR, CN, JP
+ "name": "FOMC 금리 결정", // 일정 이름
+ "importance": 3, // 1: 낮음, 2: 중간, 3: 높음
+ "forecast": "4.50%", // 예상치
+ "previous": "4.50%", // 이전값
+ "actual": null // 실제값 (발표 전: null)
+}
+```
+
+---
+
+## 주요 경제 일정 목록 (참고용)
+
+### 미국 (US)
+| 일정 | 중요도 | 발표 주기 |
+|------|--------|-----------|
+| FOMC 금리 결정 | ★★★ | 6주마다 |
+| 비농업 고용지수 (NFP) | ★★★ | 매월 첫째 금요일 |
+| CPI (소비자물가지수) | ★★★ | 매월 |
+| GDP | ★★★ | 분기 |
+| ISM 제조업 PMI | ★★★ | 매월 |
+| CB 소비자신뢰지수 | ★★★ | 매월 |
+
+### 한국 (KR)
+| 일정 | 중요도 | 발표 주기 |
+|------|--------|-----------|
+| 한국은행 기준금리 | ★★★ | 연 8회 |
+| CPI | ★★★ | 매월 |
+| 산업생산 | ★★ | 매월 |
+| 수출입 | ★★ | 매월 |
+
+### 중국 (CN)
+| 일정 | 중요도 | 발표 주기 |
+|------|--------|-----------|
+| 제조업 PMI | ★★★ | 매월 |
+| Caixin 제조업 PMI | ★★★ | 매월 |
+| GDP | ★★★ | 분기 |
+
+### 일본 (JP)
+| 일정 | 중요도 | 발표 주기 |
+|------|--------|-----------|
+| 일본은행 금리 결정 | ★★★ | 연 8회 |
+| GDP | ★★★ | 분기 |
+| 실업률 | ★★ | 매월 |
+
+---
+
+## 경제 일정 데이터 소스
+
+- [Investing.com 경제 캘린더](https://kr.investing.com/economic-calendar/)
+- [TradingEconomics](https://tradingeconomics.com/calendar)
+- [Forex Factory](https://www.forexfactory.com/calendar)
diff --git a/n8n/economic-calendar-combined.json b/n8n/economic-calendar-combined.json
new file mode 100644
index 0000000..6654dc0
--- /dev/null
+++ b/n8n/economic-calendar-combined.json
@@ -0,0 +1,156 @@
+{
+ "name": "Economic Calendar (Combined)",
+ "nodes": [
+ {
+ "parameters": {
+ "rule": {
+ "interval": [
+ {
+ "field": "cronExpression",
+ "expression": "0 3 * * *"
+ }
+ ]
+ }
+ },
+ "id": "schedule-1",
+ "name": "Schedule Trigger",
+ "type": "n8n-nodes-base.scheduleTrigger",
+ "typeVersion": 1.2,
+ "position": [250, 200]
+ },
+ {
+ "parameters": {
+ "httpMethod": "GET",
+ "path": "economic-calendar",
+ "authentication": "headerAuth",
+ "responseMode": "responseNode",
+ "options": {}
+ },
+ "id": "webhook-1",
+ "name": "API Webhook",
+ "type": "n8n-nodes-base.webhook",
+ "typeVersion": 2,
+ "position": [250, 400],
+ "webhookId": "economic-calendar",
+ "credentials": {
+ "httpHeaderAuth": {
+ "id": "economic-calendar-api-key",
+ "name": "Economic Calendar API Key"
+ }
+ }
+ },
+ {
+ "parameters": {
+ "method": "GET",
+ "url": "https://nfs.faireconomy.media/ff_calendar_thisweek.json",
+ "options": {}
+ },
+ "id": "http-1",
+ "name": "Fetch Forex Factory",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.2,
+ "position": [470, 200]
+ },
+ {
+ "parameters": {
+ "jsCode": "// Forex Factory 데이터 변환 및 DB 저장\nconst rawData = $input.all();\nconst staticData = $getWorkflowStaticData('global');\n\nconst newEvents = [];\n\nconst countryMap = {\n 'USD': 'US',\n 'JPY': 'JP',\n 'CNY': 'CN'\n};\n\nconst importanceMap = {\n 'Low': 1,\n 'Medium': 2,\n 'High': 3\n};\n\nconst getLocalDateStr = (d) => {\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n};\n\nconst convertToKST = (dateStr) => {\n const d = new Date(dateStr);\n // UTC 기준으로 KST(+9) 변환 - 서버 timezone 무관\n const kstTime = new Date(d.getTime() + 9 * 60 * 60 * 1000);\n return {\n date: `${kstTime.getUTCFullYear()}-${String(kstTime.getUTCMonth() + 1).padStart(2, '0')}-${String(kstTime.getUTCDate()).padStart(2, '0')}`,\n time: `${String(kstTime.getUTCHours()).padStart(2, '0')}:${String(kstTime.getUTCMinutes()).padStart(2, '0')}`\n };\n};\n\nfor (const item of rawData) {\n const event = item.json;\n const country = countryMap[event.country];\n \n if (!country || !['US', 'JP', 'CN'].includes(country)) continue;\n if (event.impact === 'Low') continue;\n \n const kst = convertToKST(event.date);\n \n newEvents.push({\n date: kst.date,\n time: kst.time,\n country: country,\n name: event.title,\n importance: importanceMap[event.impact] || 2,\n forecast: event.forecast || '-',\n previous: event.previous || '-',\n actual: event.actual || null\n });\n}\n\nnewEvents.sort((a, b) => {\n const dateCompare = a.date.localeCompare(b.date);\n if (dateCompare !== 0) return dateCompare;\n return a.time.localeCompare(b.time);\n});\n\n// Static Data에 저장\nstaticData.events = newEvents;\nstaticData.updated = new Date().toISOString();\nstaticData.count = newEvents.length;\n\nreturn [{\n json: {\n success: true,\n message: 'Data saved to n8n database',\n count: newEvents.length,\n updated: staticData.updated\n }\n}];"
+ },
+ "id": "code-save",
+ "name": "Transform & Save",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [690, 200]
+ },
+ {
+ "parameters": {
+ "jsCode": "// Static Data에서 이벤트 읽기 (없으면 직접 fetch)\nconst staticData = $getWorkflowStaticData('global');\n\n// 캐시 유효 시간 (6시간)\nconst CACHE_TTL = 6 * 60 * 60 * 1000;\nconst now = Date.now();\nconst lastUpdated = staticData.updated ? new Date(staticData.updated).getTime() : 0;\nconst isCacheValid = staticData.events?.length > 0 && (now - lastUpdated) < CACHE_TTL;\n\nif (isCacheValid) {\n return [{ json: {\n data: staticData.events,\n source: 'forexfactory',\n updated: staticData.updated,\n count: staticData.count,\n cached: true\n }}];\n}\n\n// 캐시가 없거나 만료된 경우 직접 fetch (n8n $http 사용)\nconst response = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://nfs.faireconomy.media/ff_calendar_thisweek.json',\n json: true\n});\nconst rawData = response;\n\nconst countryMap = { 'USD': 'US', 'JPY': 'JP', 'CNY': 'CN' };\nconst importanceMap = { 'Low': 1, 'Medium': 2, 'High': 3 };\n\nconst getLocalDateStr = (d) => {\n const y = d.getFullYear();\n const m = String(d.getMonth() + 1).padStart(2, '0');\n const day = String(d.getDate()).padStart(2, '0');\n return `${y}-${m}-${day}`;\n};\n\nconst convertToKST = (dateStr) => {\n const d = new Date(dateStr);\n // UTC 기준으로 KST(+9) 변환 - 서버 timezone 무관\n const kstTime = new Date(d.getTime() + 9 * 60 * 60 * 1000);\n return {\n date: `${kstTime.getUTCFullYear()}-${String(kstTime.getUTCMonth() + 1).padStart(2, '0')}-${String(kstTime.getUTCDate()).padStart(2, '0')}`,\n time: `${String(kstTime.getUTCHours()).padStart(2, '0')}:${String(kstTime.getUTCMinutes()).padStart(2, '0')}`\n };\n};\n\nconst newEvents = [];\nfor (const event of rawData) {\n const country = countryMap[event.country];\n if (!country || !['US', 'JP', 'CN'].includes(country)) continue;\n if (event.impact === 'Low') continue;\n \n const kst = convertToKST(event.date);\n newEvents.push({\n date: kst.date,\n time: kst.time,\n country: country,\n name: event.title,\n importance: importanceMap[event.impact] || 2,\n forecast: event.forecast || '-',\n previous: event.previous || '-',\n actual: event.actual || null\n });\n}\n\nnewEvents.sort((a, b) => {\n const dateCompare = a.date.localeCompare(b.date);\n if (dateCompare !== 0) return dateCompare;\n return a.time.localeCompare(b.time);\n});\n\n// 캐시에 저장\nstaticData.events = newEvents;\nstaticData.updated = new Date().toISOString();\nstaticData.count = newEvents.length;\n\nreturn [{ json: {\n data: newEvents,\n source: 'forexfactory',\n updated: staticData.updated,\n count: newEvents.length,\n cached: false\n}}];"
+ },
+ "id": "code-read",
+ "name": "Read Data",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [470, 400]
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ $json }}",
+ "options": {
+ "responseHeaders": {
+ "entries": [
+ {
+ "name": "Access-Control-Allow-Origin",
+ "value": "*"
+ },
+ {
+ "name": "Cache-Control",
+ "value": "max-age=300"
+ }
+ ]
+ }
+ }
+ },
+ "id": "respond-1",
+ "name": "Respond to Webhook",
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.1,
+ "position": [690, 400]
+ }
+ ],
+ "connections": {
+ "Schedule Trigger": {
+ "main": [
+ [
+ {
+ "node": "Fetch Forex Factory",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "API Webhook": {
+ "main": [
+ [
+ {
+ "node": "Read Data",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Fetch Forex Factory": {
+ "main": [
+ [
+ {
+ "node": "Transform & Save",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Read Data": {
+ "main": [
+ [
+ {
+ "node": "Respond to Webhook",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "staticData": {
+ "global": {
+ "events": [],
+ "updated": null,
+ "count": 0
+ }
+ }
+}
diff --git a/styles.css b/styles.css
index 56ac082..1eba7c7 100644
--- a/styles.css
+++ b/styles.css
@@ -34,8 +34,53 @@ body {
}
.header h1 {
- font-size: 1.5rem;
+ font-size: 1.3rem;
color: #f0b90b;
+ white-space: nowrap;
+}
+
+/* Tab Navigation */
+.tab-nav {
+ display: flex;
+ gap: 5px;
+ background: #2a2e39;
+ padding: 4px;
+ border-radius: 8px;
+}
+
+.tab-btn {
+ padding: 8px 16px;
+ background: transparent;
+ border: none;
+ color: #848e9c;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: all 0.2s;
+}
+
+.tab-btn:hover {
+ color: #d1d4dc;
+}
+
+.tab-btn.active {
+ background: #f0b90b;
+ color: #131722;
+}
+
+/* Tab Content */
+.tab-content {
+ display: none;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+/* Hide chart-only elements in calendar tab */
+.chart-only {
+ transition: opacity 0.2s;
}
.controls {
@@ -58,6 +103,71 @@ select:hover {
border-color: #f0b90b;
}
+/* Header Events (Today's Schedule) */
+.header-events {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ flex: 1;
+ justify-content: flex-start;
+ padding-left: 15px;
+}
+
+.header-events-loading {
+ color: #848e9c;
+ font-size: 12px;
+}
+
+.header-event-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: #2a2e39;
+ padding: 6px 10px;
+ border-radius: 6px;
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.header-event-item.importance-3 {
+ border-left: 3px solid #f0b90b;
+}
+
+.header-event-item.importance-2 {
+ border-left: 3px solid #848e9c;
+}
+
+.header-event-item.importance-1 {
+ border-left: 3px solid #4a4e59;
+}
+
+.header-event-time {
+ color: #848e9c;
+ font-size: 11px;
+ min-width: 35px;
+}
+
+.header-event-country {
+ font-size: 14px;
+}
+
+.header-event-name {
+ color: #d1d4dc;
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.header-event-stars {
+ color: #f0b90b;
+ font-size: 10px;
+}
+
+.header-events-empty {
+ color: #6c7284;
+ font-size: 12px;
+}
+
.price-info {
display: flex;
flex-direction: column;
@@ -495,6 +605,555 @@ select:hover {
to { transform: rotate(360deg); }
}
+/* Weekly Summary Bar */
+.weekly-summary {
+ background: #1e222d;
+ border-radius: 8px;
+ padding: 12px 15px;
+ margin-top: 10px;
+}
+
+.weekly-summary-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 10px;
+}
+
+.weekly-icon {
+ font-size: 16px;
+}
+
+.weekly-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #f0b90b;
+ flex: 1;
+}
+
+.weekly-more-btn {
+ background: transparent;
+ border: 1px solid #363a45;
+ color: #848e9c;
+ padding: 4px 10px;
+ border-radius: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.weekly-more-btn:hover {
+ border-color: #f0b90b;
+ color: #f0b90b;
+}
+
+.weekly-events {
+ display: flex;
+ gap: 15px;
+ overflow-x: auto;
+ padding-bottom: 5px;
+}
+
+.weekly-event-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: #2a2e39;
+ padding: 8px 12px;
+ border-radius: 6px;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.weekly-event-date {
+ font-size: 12px;
+ color: #848e9c;
+}
+
+.weekly-event-country {
+ font-size: 14px;
+}
+
+.weekly-event-name {
+ font-size: 13px;
+ color: #d1d4dc;
+}
+
+.weekly-event-importance {
+ color: #f0b90b;
+ font-size: 10px;
+}
+
+/* Economic Calendar Tab */
+.calendar-container {
+ padding: 10px 0;
+}
+
+.calendar-filters {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.country-filters {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.country-btn {
+ padding: 8px 14px;
+ background: #2a2e39;
+ border: 1px solid #363a45;
+ color: #848e9c;
+ font-size: 13px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.country-btn:hover {
+ border-color: #f0b90b;
+ color: #d1d4dc;
+}
+
+.country-btn.active {
+ background: #f0b90b;
+ border-color: #f0b90b;
+ color: #131722;
+}
+
+.period-filter select {
+ padding: 8px 15px;
+ background: #2a2e39;
+ border: 1px solid #363a45;
+ border-radius: 6px;
+ color: #d1d4dc;
+ font-size: 13px;
+ cursor: pointer;
+}
+
+/* Calendar List */
+.calendar-list {
+ background: #1e222d;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.calendar-date-group {
+ border-bottom: 1px solid #2a2e39;
+}
+
+.calendar-date-group:last-child {
+ border-bottom: none;
+}
+
+.calendar-date-header {
+ background: #2a2e39;
+ padding: 10px 15px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #f0b90b;
+}
+
+.calendar-event {
+ display: flex;
+ align-items: center;
+ padding: 12px 15px;
+ border-bottom: 1px solid #2a2e39;
+ gap: 12px;
+}
+
+.calendar-event:last-child {
+ border-bottom: none;
+}
+
+.calendar-event:hover {
+ background: #252930;
+}
+
+.event-time {
+ font-size: 13px;
+ color: #848e9c;
+ min-width: 50px;
+}
+
+.event-country {
+ font-size: 18px;
+ min-width: 30px;
+}
+
+.event-info {
+ flex: 1;
+}
+
+.event-name {
+ font-size: 14px;
+ color: #d1d4dc;
+ margin-bottom: 3px;
+}
+
+.event-detail {
+ font-size: 12px;
+ color: #6c7284;
+}
+
+.event-importance {
+ display: flex;
+ gap: 2px;
+}
+
+.importance-star {
+ color: #363a45;
+ font-size: 12px;
+}
+
+.importance-star.active {
+ color: #f0b90b;
+}
+
+.event-values {
+ display: flex;
+ gap: 15px;
+ min-width: 200px;
+ justify-content: flex-end;
+}
+
+.event-value {
+ text-align: right;
+}
+
+.event-value-label {
+ font-size: 10px;
+ color: #6c7284;
+ text-transform: uppercase;
+}
+
+.event-value-number {
+ font-size: 13px;
+ color: #d1d4dc;
+ font-weight: 500;
+}
+
+.event-value-number.positive {
+ color: #26a69a;
+}
+
+.event-value-number.negative {
+ color: #ef5350;
+}
+
+.loading-text {
+ padding: 30px;
+ text-align: center;
+ color: #848e9c;
+}
+
+/* Monthly Calendar Grid */
+.calendar-grid {
+ background: #1e222d;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.calendar-grid-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px;
+ background: #2a2e39;
+}
+
+.calendar-month-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #f0b90b;
+}
+
+.calendar-nav {
+ display: flex;
+ gap: 10px;
+}
+
+.calendar-nav-btn {
+ background: #363a45;
+ border: none;
+ color: #d1d4dc;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.calendar-nav-btn:hover {
+ background: #4a4e59;
+}
+
+.calendar-weekdays {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ background: #252930;
+ border-bottom: 1px solid #363a45;
+}
+
+.calendar-weekday {
+ padding: 10px;
+ text-align: center;
+ font-size: 12px;
+ font-weight: 600;
+ color: #848e9c;
+}
+
+.calendar-weekday:first-child {
+ color: #ef5350;
+}
+
+.calendar-weekday:last-child {
+ color: #2196f3;
+}
+
+.calendar-days {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+}
+
+.calendar-day {
+ min-height: 120px;
+ padding: 8px;
+ border-right: 1px solid #2a2e39;
+ border-bottom: 1px solid #2a2e39;
+ background: #1e222d;
+ overflow: hidden;
+}
+
+.calendar-day:nth-child(7n) {
+ border-right: none;
+}
+
+.calendar-day.other-month {
+ background: #171b24;
+}
+
+.calendar-day.other-month .day-number {
+ color: #4a4e59;
+}
+
+.calendar-day.today {
+ background: #252930;
+}
+
+.calendar-day.today .day-number {
+ background: #f0b90b;
+ color: #131722;
+ border-radius: 50%;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.day-number {
+ font-size: 12px;
+ color: #d1d4dc;
+ margin-bottom: 5px;
+}
+
+.calendar-day:nth-child(7n+1) .day-number {
+ color: #ef5350;
+}
+
+.calendar-day:nth-child(7n) .day-number {
+ color: #2196f3;
+}
+
+.calendar-day.other-month:nth-child(7n+1) .day-number,
+.calendar-day.other-month:nth-child(7n) .day-number {
+ color: #4a4e59;
+}
+
+.day-events {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ width: 100%;
+ overflow: hidden;
+}
+
+.day-event {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 4px;
+ background: #2a2e39;
+ border-radius: 3px;
+ font-size: 10px;
+ color: #d1d4dc;
+ overflow: hidden;
+ min-width: 0;
+ cursor: pointer;
+}
+
+.day-event:hover {
+ background: #363a45;
+}
+
+.day-event.importance-3 {
+ border-left: 2px solid #f0b90b;
+}
+
+.day-event.importance-2 {
+ border-left: 2px solid #848e9c;
+}
+
+.day-event.importance-1 {
+ border-left: 2px solid #4a4e59;
+}
+
+.day-event-time {
+ font-size: 9px;
+ color: #848e9c;
+ min-width: 32px;
+}
+
+.day-event-country {
+ font-size: 11px;
+}
+
+.day-event-name {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.day-event-stars {
+ font-size: 8px;
+ color: #f0b90b;
+ margin-left: 2px;
+}
+
+.day-event-more {
+ font-size: 10px;
+ color: #848e9c;
+ padding: 2px 4px;
+ cursor: pointer;
+}
+
+.day-event-more:hover {
+ color: #f0b90b;
+}
+
+/* Events Modal */
+.events-modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.events-modal {
+ background: #1e222d;
+ border-radius: 12px;
+ min-width: 320px;
+ max-width: 90vw;
+ max-height: 80vh;
+ overflow: hidden;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ border: 1px solid #2a2e39;
+}
+
+.events-modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ background: #252930;
+ border-bottom: 1px solid #2a2e39;
+}
+
+.events-modal-header h3 {
+ margin: 0;
+ font-size: 16px;
+ color: #d1d4dc;
+}
+
+.events-modal-close {
+ background: none;
+ border: none;
+ color: #848e9c;
+ font-size: 24px;
+ cursor: pointer;
+ padding: 0;
+ line-height: 1;
+}
+
+.events-modal-close:hover {
+ color: #f0b90b;
+}
+
+.events-modal-body {
+ padding: 16px 20px;
+ max-height: 60vh;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.modal-event {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ background: #2a2e39;
+ border-radius: 6px;
+ font-size: 13px;
+ color: #d1d4dc;
+}
+
+.modal-event.importance-3 {
+ border-left: 3px solid #f0b90b;
+}
+
+.modal-event.importance-2 {
+ border-left: 3px solid #848e9c;
+}
+
+.modal-event.importance-1 {
+ border-left: 3px solid #4a4e59;
+}
+
+.modal-event-time {
+ color: #848e9c;
+ font-size: 12px;
+ min-width: 40px;
+}
+
+.modal-event-country {
+ font-size: 16px;
+}
+
+.modal-event-name {
+ flex: 1;
+ color: #d1d4dc;
+}
+
+.modal-event-stars {
+ color: #f0b90b;
+ font-size: 11px;
+}
+
/* Responsive */
@media (max-width: 768px) {
.header {
@@ -502,10 +1161,26 @@ select:hover {
gap: 10px;
}
+ .title-section {
+ flex-direction: column;
+ gap: 10px;
+ width: 100%;
+ }
+
.header h1 {
font-size: 1.2rem;
}
+ .header-events {
+ max-width: 100%;
+ justify-content: flex-start;
+ padding: 0;
+ }
+
+ .header-event-name {
+ max-width: 80px;
+ }
+
#main-chart {
height: 350px;
}
@@ -520,4 +1195,14 @@ select:hover {
flex-wrap: wrap;
gap: 8px;
}
+
+ .calendar-event {
+ flex-wrap: wrap;
+ }
+
+ .event-values {
+ width: 100%;
+ justify-content: flex-start;
+ margin-top: 8px;
+ }
}