diff --git a/.gitignore b/.gitignore index 24e8d72d..316c3116 100644 --- a/.gitignore +++ b/.gitignore @@ -365,5 +365,7 @@ FodyWeavers.xsd /EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode /EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs /EnvelopeGenerator.Tests.Application/annotations.json +/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md +/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md /EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output /EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8f57e84a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,263 @@ +# EnvelopeGenerator - Agent Guide + +## Must Read First +- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status +- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate + +## Active Architecture (Post-Migration) + +**Frontend:** Blazor Auto (Server+WASM hybrid) +- **WebUI** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend +- **WebUI.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic + +**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0) + +**Proxy:** YARP in WebUI routes `/api/*` → `localhost:8088` (API) + +### Deprecated Projects - DO NOT USE +- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to WebUI) +- `EnvelopeGenerator.Web` - Razor Pages (replaced by WebUI) +- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference + +## Development Commands + +### Run Both Projects (Required) +```powershell +# Terminal 1 - API Backend +cd EnvelopeGenerator.API +dotnet run + +# Terminal 2 - Blazor Frontend +cd EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI +dotnet run +``` + +**Critical:** Both must run simultaneously. WebUI proxy forwards `/api/*` to API. + +### Build +```powershell +dotnet build EnvelopeGenerator.sln +``` + +## Project Boundaries + +``` +EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.) +EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers) +EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories +EnvelopeGenerator.API/ # Controllers, endpoints +EnvelopeGenerator.WebUI/ # Server-side Blazor components + ├─ Components/Pages/ # @rendermode InteractiveServer +EnvelopeGenerator.WebUI.Client/ # Client-side WASM components + ├─ Pages/ # @rendermode InteractiveWebAssembly + ├─ Services/ # HTTP API clients + ├─ Models/ # DTOs +``` + +## Route Structure (Critical) + +| Route | File Location | Render Mode | Purpose | +|-------|--------------|-------------|---------| +| `/` | `WebUI.Client/Pages/Index.razor` | WASM | Landing page | +| `/sender/login` | `WebUI.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth | +| `/sender` | `WebUI.Client/Pages/EnvelopeSenderPage.razor` | WASM | Sender dashboard | +| `/envelope/login/{key}` | `WebUI.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth | +| `/envelope/{key}` | `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing | + +**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM. + +## Coordinate System (CRITICAL) + +**Database stores INCHES** (GdPicture14 native). Origin: top-left, Y-axis down. + +### Conversions +```csharp +// Database (INCHES) → PDF Points +float points = inches * 72; + +// Database (INCHES) → DevExpress DX +float dx = inches * 100; + +// PDF.js Pixels → Database (INCHES) +float inches = (pixelX / canvasWidth) * pageWidthInches; +``` + +**A4 Page:** 8.27" wide × 11.69" tall = 595pt × 842pt + +**Signature Field Size:** 1.77" × 1.96" (FIXED, do not change) + +**Evidence:** See `COPILOT_CONTEXT.md` lines 158-185, `EnvelopeGenerator.Form/frmFieldEditor.vb` + +## API Architecture Quirks + +### Monolithic Endpoint (Avoid for UI) +`POST /api/EnvelopeReceiver` - Creates envelope+document+receivers+fields atomically. +- **Use case:** External API consumers +- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates) + +### Missing Granular Endpoints (Need to Create) +``` +POST /api/Envelope/draft # Create draft envelope +PUT /api/Envelope/{id} # Update metadata +DELETE /api/Envelope/{id} # Delete with reason +POST /api/Envelope/{id}/document # Upload PDF +POST /api/Envelope/{id}/receivers # Add receiver +POST /api/Envelope/{id}/signature-fields # Place signature field +POST /api/Envelope/{id}/send # Send to receivers +``` + +See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements. + +## Status Color Coding + +Form app uses DevExpress `CustomDrawCell`. WebUI needs CSS: + +```css +.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */ +.envelope-row.status-queued, +.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */ +.envelope-row.status-completed { background-color: #81C784; } +.envelope-row.status-deleted, +.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */ +``` + +## Configuration + +### YARP Proxy (`WebUI/yarp.json`) +Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088` + +### PDF.js Settings (`WebUI/wwwroot/appsettings.json`) +```json +{ + "PdfViewerOptions": { + "ThumbnailBaseScale": 0.75, + "ThumbnailEnableHiDPI": true, + "MainCanvasEnableHiDPI": true, + "ZoomStepPercentage": 5 + } +} +``` + +### API Config (`API/appsettings.json`) +- `ConnectionStrings:Default` - SQL Server DB +- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`) +- `Cache:SignatureCacheExpiration` - Signature persistence timeout +- `PSPDFKitLicenseKey` - **DEPRECATED** (use PDF.js instead) + +## Migration Status + +### Complete ✅ +- Receiver login/authentication +- PDF viewing with PDF.js (HiDPI, zoom, thumbnails) +- Signature capture (draw/type/image) +- Signature caching (Redis/SQL) +- Sender login + +### Missing (High Priority) ❌ +- Sender dashboard (`/sender`) - Empty stub +- Envelope editor (`/sender/envelope/{id}`) +- Signature field placement tool (PDF.js + draggable overlays) +- Granular API endpoints (draft, receivers, fields) +- Master-detail grids for receivers/history + +## Common Mistakes (DO NOT REPEAT) + +| Mistake | Why Wrong | +|---------|-----------| +| Using iText7 in receiver pages | GPL license issue. Use PDF.js overlays. | +| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. | +| `@rendermode InteractiveWebAssembly` on PDF viewers | DevExpress DxPdfViewer requires server-side rendering. | +| Hardcoded quality in PDF.js | Use `appsettings.json` `PdfViewerOptions`. | +| Coordinates in points/pixels for DB | Database uses INCHES. Convert before save. | +| `BottomMarginBand` for signatures | Repeats on every page. Use `DetailBand`. | + +## Testing + +**No automated tests exist yet.** + +Manual testing workflow: +1. Start API (`dotnet run` in `EnvelopeGenerator.API`) +2. Start WebUI (`dotnet run` in `EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI`) +3. Navigate to `https://localhost:5131` (or check console output for port) +4. Test sender login at `/sender/login` +5. Test receiver flow at `/envelope/login/{envelopeKey}` + +## Database + +**SQL Server** (DD_ECM) +- Connection string in `API/appsettings.json` +- EF Core migrations NOT used (manual SQL scripts) +- Stored procedures: `PRSIG_*` prefix + +**Key Tables:** +- `TBSIG_ENVELOPE` - Envelope metadata +- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments +- `TBSIG_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES) +- `TBSIG_RECEIVER` - Receiver registry +- `TBSIG_DOCUMENT` - PDF binary data +- `TBSIG_ENVELOPE_HISTORY` - Audit trail + +## DevExpress + +**License:** Commercial (v25.2.3) +**Components Used:** +- `DxGrid` - Master-detail grids +- `DxPdfViewer` - Server-side PDF rendering +- `DxPopup` - Modal dialogs +- `DxToolbar` - Action bars +- `DxFormLayout` - Forms + +**Theme:** Blazing Berry (default) + +## JavaScript Interop + +**PDF Viewer:** `wwwroot/js/pdf-viewer.js` +```javascript +window.pdfViewer = { + initialize(canvasId, pdfDataUrl, dotNetRef), + renderPage(num), + renderSignatureButtons(signatures, pageNum, dotNetRef), + applySignature(signatureId, dataUrl, fullName, position, place), + zoomIn(), zoomOut(), dispose() +} +``` + +**Signature Pad:** `wwwroot/js/receiver-signature.js` +```javascript +window.receiverSignature = { + initializeDrawPad(canvasId, dotNetRef), + getSignatureDataUrl(canvasId), + clearPad(canvasId) +} +``` + +## Multi-Envelope Support + +Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies: +``` +AuthTokenSignFLOWReceiver.{envelopeKey} +``` + +Each envelope maintains independent authentication state. + +## External Dependencies + +**CDN:** +- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js` + +**NuGet (WebUI.Client):** +- `DevExpress.Blazor.*` 25.2.3 +- `SkiaSharp.*` 3.119.1 (WASM rendering) + +**External Services:** +- LDAP/AD authentication (optional) +- GTX Messaging (SMS 2FA) +- Email dispatcher (signFlow) + +## Environment Variables + +None required. All config in `appsettings.json`. + +**Local dev ports:** +- API: `https://localhost:8088` +- WebUI: `https://localhost:5131` or `http://localhost:7192` diff --git a/COPILOT_CONTEXT.md b/COPILOT_CONTEXT.md index a683bdd1..81ecf93b 100644 --- a/COPILOT_CONTEXT.md +++ b/COPILOT_CONTEXT.md @@ -1,489 +1,351 @@ -# EnvelopeGenerator — AI Context Reference +# EnvelopeGenerator — Current Workspace Context ## Purpose -Digital document signing system with **unified Blazor WASM frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs. +Digital document signing system for senders and receivers. -**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed) +- Senders authenticate, view envelope lists, and manage envelope workflows. +- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer. +- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes. +- Primary UI/PDF libraries are `DevExpress` and `PDF.js`. --- -## Deployment Architecture +## Active Application Structure -**Two Presentation Projects (Both Required):** +### Main Host +**Primary active application:** `EnvelopeGenerator.Server` -1. **EnvelopeGenerator.API** (ASP.NET Core Web API) - - Runs independently (development & production) - - **YARP Reverse Proxy** configured via `yarp.json` - - Proxies requests to: - - `EnvelopeGenerator.ReceiverUI` (Blazor WASM) - - External Auth.API service - - Serves as single entry point for all requests +`EnvelopeGenerator.Server` is the current runtime host and contains: +- Blazor server host +- WebAssembly host integration +- API controllers +- authentication/authorization setup +- Swagger/Scalar setup +- YARP reverse proxy configuration +- DevExpress server-side services +- SQL Server distributed cache setup -2. **EnvelopeGenerator.ReceiverUI** (Blazor WebAssembly) - - Runs on separate host/port - - Accessed **only through API proxy** (not directly) - - Serves static files (HTML, JS, CSS, WASM) +### Client Project +**Client UI project:** `EnvelopeGenerator.Server.Client` -**Request Flow:** -``` -Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM) - ? Auth.API:9090 (External Auth Service) -``` +This project contains: +- WebAssembly-rendered pages +- client-side services +- client models and options +- sender and receiver login flows -**Configuration:** `EnvelopeGenerator.API/yarp.json` +### Other Projects +- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic +- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions +- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services +- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities +- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server` + +### Legacy / Do Not Touch +- `EnvelopeGenerator.Service` +- `EnvelopeGenerator.Form` +- `EnvelopeGenerator.BBTests` +- `EnvelopeGenerator.CommonServices` --- -## ReceiverUI Route Structure +## Current Hosting Model -### Root Route -| Route | File | Purpose | -|---|---|---| -| `/` | `Index.razor` | Application entry point (landing page). | +`EnvelopeGenerator.Server/Program.cs` currently configures: +- `AddRazorComponents()` with both interactive server and interactive WebAssembly components +- `AddControllers()` and `MapControllers()` +- JWT authentication for sender and receiver flows +- cookie authentication +- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver` +- `AddReverseProxy()` with `yarp.json` +- Swagger / OpenAPI / Scalar +- distributed SQL Server cache +- DevExpress Blazor and DevExpress PDF Viewer server-side services +- request localization middleware -### Sender Routes -| Route | File | Purpose | -|---|---|---| -| `/sender/login` | `LoginSenderPage.razor` | Username/password authentication | -| `/sender` | `EnvelopeSenderPage.razor` | Sender dashboard (envelope list) | - -### Receiver Routes -| Route | File | Purpose | -|---|---|---| -| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | Access code authentication for specific envelope | -| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | View & sign envelope (PDF.js viewer) | - -**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication). +This means the active app is a **merged UI + API host**. --- -## Architecture Evolution +## Reverse Proxy -### Old Architecture (Deprecated) -- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit) -- **Receiver UI:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM + PDF.js) -- **Backend:** `EnvelopeGenerator.API` +**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json` -### Current Architecture -- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM) — **Both Senders & Receivers** -- **Backend:** `EnvelopeGenerator.API` — **Both Senders & Receivers** -- **Libraries:** DevExpress + PDF.js -- **PSPDFKit:** **REMOVED** +Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy. + +Configured routes forward: +- `POST /api/auth` -> AuthHub `/api/auth/sign-flow` +- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true` --- -## Solution Structure +## Active Routes and Files -| Project | Target | Purpose | -|---|---|---| -| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. | -| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **Unified Blazor WebAssembly Frontend**. UI for **both Senders & Receivers**. YARP proxy to API. | -| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. | -| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. | -| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. | -| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. | -| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in ReceiverUI). | -| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. | -| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** | - ---- - -## Localization & Culture Management - -**Current Architecture:** Blazor WebAssembly (client-side culture management) - -### Implementation Details - -**Culture Storage:** -- Culture preference stored in browser's `localStorage` (key: `AppCulture`) -- Managed by `CultureService.cs` (ReceiverUI/Services) -- Supported cultures: `de-DE`, `en-US`, `fr-FR` - -**Culture Initialization:** -- **Location:** `Program.cs` (lines 53-57) -- Sets `CultureInfo.DefaultThreadCurrentCulture/UICulture` **before** app runs -- **WASM-Safe:** Each user has isolated browser instance - -**Language Selector:** -- **Component:** `LanguageSelector.razor` (ReceiverUI/Shared) -- Displays flag icon + language name -- Changes culture via `CultureService.SetCultureAsync()` -- Navigates with `forceLoad: false` (smooth transition, no page reload) - -### ⚠️ MIGRATION WARNING: Blazor Server/Auto - -**Current approach is WASM-specific and will break in Server/Auto render modes!** - -**Why it breaks:** -- `Program.cs:53-57` sets **global** `DefaultThreadCurrentCulture` -- In Server/Auto, one app instance serves **all users** -- User A selects German → User B sees German too (shared state) -- Thread-safety issues and culture conflicts - -**Migration Checklist (when moving to Server/Auto):** - -1. **Remove global culture initialization** from `Program.cs` (lines 53-57) - - See detailed warning comment in the code - -2. **Add RequestLocalizationMiddleware** (Server-side approach): - ```csharp - app.UseRequestLocalization(options => { - options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" }; - options.SupportedUICultures = options.SupportedCultures; - options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider()); - }); - ``` - -3. **OR** Use **per-circuit culture** (Blazor Server approach): - - Store culture in circuit-scoped service - - Use `CascadingParameter` to distribute to components - - See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization - -4. **Update `LanguageSelector.razor`:** - - Remove manual `CultureInfo.DefaultThreadCurrentCulture` assignment - - Use middleware/circuit culture provider instead - -5. **Update `CultureService.cs`:** - - Integrate with Server-side culture provider - - May need to store in cookies instead of localStorage - -**References:** -- Microsoft Docs: [Blazor Globalization/Localization](https://learn.microsoft.com/aspnet/core/blazor/globalization-localization) -- Current implementation: `Program.cs`, `CultureService.cs`, `LanguageSelector.razor` - ---- - -## Key Files & Routes - -| File | Route/Purpose | -|---|---| -| `ReceiverUI/Pages/Index.razor` | `/` — Application entry point (landing page). | -| `ReceiverUI/Pages/EnvelopeSenderPage.razor` | `/sender` — Sender dashboard (envelope list). | -| `ReceiverUI/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` — Receiver PDF viewer & signing. | -| `ReceiverUI/Pages/LoginSenderPage.razor` | `/sender/login` — Sender username/password auth. | -| `ReceiverUI/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` — Receiver access code auth. | -| `ReceiverUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). | -| `ReceiverUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). | -| `ReceiverUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. | -| `ReceiverUI/Services/AuthService.cs` | Receiver + Sender authentication. | -| `ReceiverUI/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). | -| `API/Controllers/CacheController.cs` | Signature cache endpoints. | - ---- - -## Coordinate System — CRITICAL - -**Database Format:** INCHES (GdPicture14 native) -**Origin:** Top-left corner -**Axes:** X right, Y down - -### Conversion Formulas - -| From INCHES to | Formula | Example | -|---|---|---| -| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX | -| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt | -| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` | - -**A4 Dimensions:** -- Width: 8.27" = 595pt = 827 DX -- Height: 11.69" = 842pt = 1169 DX - -### Unit Systems - -| System | Unit | Origin | Y-Axis | +### WebAssembly Pages (`EnvelopeGenerator.Server.Client`) +| Route | File | Render Mode | Purpose | |---|---|---|---| -| **Database (GdPicture14)** | Inches | Top-left | Down | -| PDF.js | Pixels | Top-left | Down | -| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) | -| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** | +| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page | +| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login | +| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard | +| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login | + +### Server Pages (`EnvelopeGenerator.Server`) +| Route | File | Render Mode | Purpose | +|---|---|---|---| +| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page | +| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page | +| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering | +| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page | --- -## EnvelopeReceiver — PDF.js Viewer & Signing +## Current API Location -**Route:** `/envelope/{EnvelopeKey}` -**Tech:** PDF.js 3.11.174 + Blazor WASM + configurable quality -**File:** `ReceiverUI/Pages/EnvelopeReceiverPage.razor` +The active application exposes controllers from: +`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers` -### Key Features -1. HiDPI/Retina support (4x quality) -2. Configurable quality (`appsettings.json`) -3. Unlimited zoom (50%-300%) -4. Ctrl+Wheel global zoom -5. Resizable thumbnail sidebar (150-400px, localStorage) -6. Responsive (desktop/mobile) +Current controller set includes: +- `AnnotationController` +- `AuthController` +- `CacheController` +- `ConfigController` +- `DocumentController` +- `EmailTemplateController` +- `EnvelopeController` +- `EnvelopeReceiverController` +- `EnvelopeTypeController` +- `HistoryController` +- `LocalizationController` +- `ReadOnlyController` +- `ReceiverController` +- `SignatureController` +- `TfaRegistrationController` -### Configuration -**File:** `ReceiverUI/wwwroot/appsettings.json` - -```json -{ - "PdfViewer": { - "ThumbnailBaseScale": 0.75, - "ThumbnailEnableHiDPI": true, - "MainCanvasEnableHiDPI": true, - "ZoomStepPercentage": 5 - } -} -``` - -### JavaScript API -**File:** `ReceiverUI/wwwroot/js/pdf-viewer.js` - -```javascript -window.pdfViewer = { - initialize(canvasId, pdfDataUrl, dotNetRef), - renderPage(num), - renderSignatureButtons(signatures, pageNum, dotNetRef), - applySignature(signatureId, dataUrl, fullName, position, place), - zoomIn(), zoomOut(), dispose() -} -``` +Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly. --- -## Signature Workflow — EnvelopeReceiver +## Authentication Model -**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only. +### Sender +Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`. -### Workflow Steps +Key sender endpoints: +- `POST /api/auth?cookie=true` — login +- `GET /api/auth/check` — current sender access check +- `POST /api/auth/logout` — logout -1. **Page Load:** - - Check `SignatureCacheService` for cached signature - - If cached ? skip popup, load signature - - If not ? show automatic popup (mandatory) +### Receiver +Receiver authentication is **per envelope**. -2. **Signature Popup (DxPopup):** - - **Cannot close** (no X, no ESC, no outside-click) - - **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload) - - **Required:** Full name, Place - - **Optional:** Position - - **Save ?** Store in `_capturedSignature`, cache via API +Key receiver endpoints used by client services: +- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code +- `GET /api/auth/check/envelope/{envelopeKey}` — check access +- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope -3. **Signature Buttons:** - - Render purple "Unterschreiben" buttons at signature field positions - - Coordinates: INCHES ? POINTS ? Pixels (scaled) - - File: `pdf-viewer.js` ? `renderSignatureButtons()` +Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from: +- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern -4. **Apply Signature (Click "Unterschreiben"):** - - JS: Remove button, create HTML overlay - - Format: Image + separator + text (Name, Position, Place, Date) - - **NOT stamped on PDF bytes** (visual overlay only) +### Receiver Server-Side Authorization +`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization. -5. **Re-rendering:** - - Zoom/Page change ? recalculate button positions - - Session state: `_capturedSignature` (lost on refresh) +It uses: +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs` -### Data Model -**File:** `ReceiverUI/Models/SignatureCaptureDto.cs` +Behavior: +- tries the current `HttpContext.User` +- if needed, reads the per-envelope receiver cookie directly +- validates the JWT with the receiver auth scheme +- verifies the token subject matches the route envelope key + +--- + +## Receiver Page Data Loading + +Main server-side page data service: +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs` + +This service loads directly via MediatR and distributed cache: +- document bytes +- receiver envelope data +- signature placeholders +- cached signature data + +For signature placeholders, the service: +- reads document receiver elements +- filters them for the authenticated receiver +- converts coordinates to `UnitOfLength.Point` before UI use + +--- + +## Receiver PDF Viewer + +**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` + +Current receiver viewer characteristics: +- route: `/envelope/{EnvelopeKey}` +- render mode: `InteractiveServer` +- PDF rendering: `PDF.js` +- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset +- signature popup: `DxPopup` +- thumbnail sidebar: resizable and stored in `localStorage` + +### JS Assets +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js` + +### CSS +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css` + +### PDF.js CDN +- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js` +- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css` + +--- + +## Signature Workflow + +Receiver signatures are handled as a **viewer overlay workflow**. + +### Current behavior +1. Server-side authorization validates receiver access. +2. The page loads document bytes, receiver data, signature placeholders, and cached signature state. +3. If no cached signature exists, the signature popup opens automatically. +4. Receiver creates signature using one of three tabs: + - draw + - text + - image +5. Required metadata: + - full name + - place +6. Optional metadata: + - position +7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer. + +### Important note +Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer. + +### Signature DTO +`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs` ```csharp public sealed record SignatureCaptureDto { - public required string DataUrl { get; init; } // base64 PNG + public required string DataUrl { get; init; } public required string FullName { get; init; } - public string Position { get; init; } = ""; // Optional + public string Position { get; init; } = ""; public required string Place { get; init; } } ``` --- -## Signature Caching +## Signature Cache -**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL) +### Active cache model +The current receiver page cache flow is handled directly in the server project through: +- `EnvelopeReceiverPageDataService` +- `IDistributedCache` +- SQL Server distributed cache configuration from `Program.cs` -### API Endpoints -**Controller:** `API/Controllers/CacheController.cs` +### Cache key format +Current server-side key prefix: +- `envelope-generator.receiver-ui.signature:{receiverSignature}` -- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save -- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load -- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete +This is different from an envelope-key-only cache convention. -**Cache Key Format:** -``` -signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey} -``` +### Config +`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs` +- section name: `Cache` +- option: `SignatureCacheExpiration` -**Configuration:** `appsettings.json` -```json -{ - "Cache": { - "SignatureCacheExpiration": null // or "02:00:00" for 2h - } -} -``` - -### Service -**File:** `ReceiverUI/Services/SignatureCacheService.cs` - -```csharp -public class SignatureCacheService { - Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature); - Task GetSignatureAsync(string envelopeKey); - Task DeleteSignatureAsync(string envelopeKey); -} -``` - -**Error Handling:** Fire-and-forget saves, graceful degradation on load failure. +### Related controller +A cache API controller also exists in: +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs` --- -## Sender Login +## Sender Dashboard -**Route:** `/sender/login` -**File:** `ReceiverUI/Pages/LoginSenderPage.razor` -**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme +**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` -### AuthService Extension -**File:** `ReceiverUI/Services/AuthService.cs` +Current behavior: +- checks sender access through `AuthService.CheckSenderAccessAsync()` +- redirects to `/sender/login` when unauthorized +- loads envelope list through client `EnvelopeService` +- separates envelopes into active/completed tabs +- uses `DevExpress DxGrid` -```csharp -public enum SenderLoginResult { Success, InvalidCredentials, Error } - -public async Task LoginSenderAsync(string username, string password) { - var response = await http.PostAsJsonAsync( - $"{_api.BaseUrl}/api/auth?cookie=true", - new { username, password }); - - return response.StatusCode switch { - HttpStatusCode.OK => SenderLoginResult.Success, - HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials, - _ => SenderLoginResult.Error - }; -} -``` - -### API Integration -**Endpoint:** `POST /api/auth?cookie=true` - -**Request:** -```json -{ "username": "TekH", "password": "***" } -``` - -**Response:** -- `200 OK` ? Cookie set, redirect to `/sender` -- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten" -- Other ? Show error: "Serverfehler" - -**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict - -### UI Flow -1. User enters username + password -2. Click "Anmelden" or press Enter -3. Call `AuthService.LoginSenderAsync()` -4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)` -5. Error ? Display alert +The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page. --- -## Receiver Login +## Localization -**Route:** `/envelope/login/{EnvelopeKey}` -**File:** `ReceiverUI/Pages/LoginReceiverPage.razor` +Current server host localization setup in `Program.cs`: +- supported cultures: `de-DE`, `en-US` +- request localization middleware is enabled +- `QueryStringRequestCultureProvider` is added +- cookie-based localization services are registered via `AddCookieBasedLocalizer()` -**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session. - -### AuthService Method -```csharp -public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error } - -public async Task LoginEnvelopeReceiverAsync(string key, string accessCode) { - var form = new MultipartFormDataContent(); - form.Add(new StringContent(accessCode), "AccessCode"); - - var response = await http.PostAsync( - $"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form); - - return response.StatusCode switch { - HttpStatusCode.OK => EnvelopeLoginResult.Success, - HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode, - HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound, - _ => EnvelopeLoginResult.Error - }; -} -``` - -**Success:** Redirect to `/envelope/{key}` +Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host. --- -## NuGet Packages (ReceiverUI) +## Coordinate System -| Package | Version | Purpose | -|---|---|---| -| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) | -| `SkiaSharp.*` | 3.119.1 | WASM rendering | -| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) | +### Source data +Database signature coordinates are still based on: +- **unit:** inches +- **origin:** top-left +- **axes:** X right, Y down -**External CDN:** -- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js` +### Relevant conversions +- inches -> PDF points: `x_pt = x_inches * 72` +- inches -> DevExpress DX units: `x_dx = x_inches * 100` + +### Current receiver page behavior +The server page data service converts signature placeholders to **points** before sending them into the viewer workflow. + +### Unit systems to keep in mind +| System | Unit | Origin | Y-axis | +|---|---|---|---| +| Database | Inches | Top-left | Down | +| PDF.js display | Pixels | Top-left | Down | +| PDF points | Points | Depends on PDF model | Depends on consumer | +| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage | --- -## Mistakes History — Do NOT Repeat +## Key Services and Files -| Mistake | Why Wrong | -|---|---| -| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. | -| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. | -| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. | -| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. | -| Over-designed UI (gradients/badges) | User prefers simple text labels. | -| Ignoring "revert" instructions | Revert HTML structure, not just CSS. | -| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. | -| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. | +### Client services +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs` + +### Server services +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs` + +### Server config and host files +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json` --- -## Development Notes +## Working Rules for This Workspace -### Deprecated Projects -**DO NOT USE:** -- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified ReceiverUI -- PSPDFKit — Removed, use PDF.js + DevExpress instead - -### Legacy Projects (VB.NET) -**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests` - -### Signature Coordinate Evidence -**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET) - -```vb -Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches -Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches - -Sub LoadAnnotation(pElement As Signature, ...) - oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment - oAnnotation.Top = CSng(pElement.Y) -End Sub -``` - -Proves database uses INCHES natively. +- Treat `EnvelopeGenerator.Server` as the active main application host. +- Treat `EnvelopeGenerator.Server.Client` as the active client UI project. +- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references. +- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked. +- Do not modify the legacy VB.NET projects unless explicitly requested. +- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`. +- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`. --- -## Quick Reference - -### When working with coordinates: -1. **Database ? UI:** INCHES × 72 = PDF Points -2. **UI ? Display:** Points × scale = Pixels -3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up) - -### When adding features: -1. Check `Mistakes History` first -2. Prefer simplicity over complexity -3. Use `appsettings.json` for configuration -4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry) -5. **Unified frontend:** ReceiverUI serves both Senders and Receivers - -### When debugging: -1. **Coordinates:** Always check unit system (inches/points/pixels) -2. **Authentication:** Check cookie name/domain/SameSite -3. **Cache:** Check Redis/SQL connection + key format -4. **Frontend confusion:** Only use ReceiverUI (Web is deprecated) - ---- - -**Last Updated:** Session 19 (Razor file naming convention + Index route proxy) +**Last Updated:** 2026-06-29 diff --git a/EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs b/EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs index 80472540..55ee0f71 100644 --- a/EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs +++ b/EnvelopeGenerator.API/Controllers/EnvelopeReceiverController.cs @@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase if (reader.Read()) { bool outSuccess = reader.GetBoolean(0); + if (!outSuccess) + _logger.LogWarning( + "PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}", + document.Id, rcv.Id, sign.Page); } } #endregion @@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase #region Create history // ENV_UID, STATUS_ID, USER_ID, string sql_hist = @" - USE [DD_ECM] - DECLARE @OUT_SUCCESS bit; EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE] @@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase if (reader.Read()) { bool outSuccess = reader.GetBoolean(0); + if (!outSuccess) + _logger.LogWarning( + "PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}", + envelope.Uuid); } } #endregion diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs index 601ac1b0..822a57fe 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeDto.cs @@ -1,6 +1,7 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using DigitalData.UserManager.Application.DTOs.User; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; +using EnvelopeGenerator.Application.Common.Dto.History; using EnvelopeGenerator.Domain.Constants; using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Interfaces; @@ -129,4 +130,9 @@ public record EnvelopeDto : IEnvelope /// /// public IEnumerable? EnvelopeReceivers { get; set; } + + /// + /// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc. + /// + public IEnumerable? Histories { get; set; } } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs index 34a74c2b..b75722d9 100644 --- a/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs +++ b/EnvelopeGenerator.Application/Common/Dto/EnvelopeReceiver/EnvelopeReceiverDto.cs @@ -1,5 +1,6 @@ using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes; using EnvelopeGenerator.Application.Common.Dto.Receiver; +using EnvelopeGenerator.Domain.Constants; namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; @@ -73,4 +74,13 @@ public record EnvelopeReceiverDto /// /// public bool HasPhoneNumber { get; init; } + + /// + /// Indicates whether this receiver has signed the envelope. + /// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history. + /// + public bool Signed => Envelope?.Histories?.Any(h => + h.Receiver?.Id == ReceiverId && + h.Status == EnvelopeStatus.DocumentSigned + ) ?? false; } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Common/Query/ReceiverQueryBase.cs b/EnvelopeGenerator.Application/Common/Query/ReceiverQueryBase.cs index 589598a3..bfb6a26c 100644 --- a/EnvelopeGenerator.Application/Common/Query/ReceiverQueryBase.cs +++ b/EnvelopeGenerator.Application/Common/Query/ReceiverQueryBase.cs @@ -1,4 +1,6 @@ -namespace EnvelopeGenerator.Application.Common.Query; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EnvelopeGenerator.Application.Common.Query; /// /// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen. @@ -29,5 +31,6 @@ public record ReceiverQueryBase /// , , or is not null. /// Usage example: The query can be executed only if at least one criterion is specified. /// - public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null; + [NotMapped] + public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null; } \ No newline at end of file diff --git a/EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs b/EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs index af136376..8d59d6aa 100644 --- a/EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs +++ b/EnvelopeGenerator.Application/Common/SQL/EnvelopeReceiverAddReadSQL.cs @@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL /// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE, /// public string Raw => @" - DECLARE @OUT_RECEIVER_ID int + DECLARE @OUT_RECEIVER_ID bigint EXEC [dbo].[PRSIG_API_CREATE_RECEIVER] {0}, diff --git a/EnvelopeGenerator.Application/DependencyInjection.cs b/EnvelopeGenerator.Application/DependencyInjection.cs index 2c33f529..23715f65 100644 --- a/EnvelopeGenerator.Application/DependencyInjection.cs +++ b/EnvelopeGenerator.Application/DependencyInjection.cs @@ -51,8 +51,8 @@ public static class DependencyInjection services.Configure(config.GetSection(nameof(TotpSmsParams))); services.AddHttpClientService(config.GetSection(nameof(GtxMessagingParams))); - services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddScoped(); // Changed: Singleton → Scoped + services.TryAddScoped(); // Changed: Singleton → Scoped services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/EnvelopeGenerator.Application/Receivers/Queries/ReadReceiverQuery.cs b/EnvelopeGenerator.Application/Receivers/Queries/ReadReceiverQuery.cs index b1255514..53d900ef 100644 --- a/EnvelopeGenerator.Application/Receivers/Queries/ReadReceiverQuery.cs +++ b/EnvelopeGenerator.Application/Receivers/Queries/ReadReceiverQuery.cs @@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query; using MediatR; using EnvelopeGenerator.Domain.Entities; using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; namespace EnvelopeGenerator.Application.Receivers.Queries; @@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries; /// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen. /// um spezifische Informationen über einen Empfänger abzurufen. /// -public record ReadReceiverQuery : ReceiverQueryBase, IRequest>; +public record ReadReceiverQuery : ReceiverQueryBase, IRequest> +{ + /// + /// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers + /// + public virtual string? EmailAddressSearch { get; set; } + + /// + /// Checks whether any of the specified query criteria have a value. + /// + /// + /// This property returns true if at least one of the fields + /// , , , or is not null. + /// Usage example: The query can be executed only if at least one criterion is specified. + /// + [NotMapped] + public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria; +} /// /// @@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler r.EmailAddress == email); } + if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch)) + { + query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%")); + } + if (request.Signature is string signature) { query = query.Where(r => r.Signature == signature); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Adjustment.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Adjustment.cs new file mode 100644 index 00000000..affe7385 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Adjustment.cs @@ -0,0 +1,44 @@ +namespace EnvelopeGenerator.Server.Client.Data { + public class Adjustment + { + public static Adjustment CreateBalanceForward(DateTime dt, int random) + { + var rnd = new DeterministicRandom(random); + Adjustment res = new Adjustment(); + res.currentDateTime = dt; + res.currentDescription = "Balance Forward"; + res.currentAmount = rnd.Random(10, 300) * 10; + return res; + } + public static Adjustment CreatePayment(DateTime dt, int random) + { + var rnd = new DeterministicRandom(random); + Adjustment res = new Adjustment(); + res.currentDateTime = dt; + res.currentDescription = "Payment"; + res.currentAmount = -rnd.Random(1, 40) * 10; + return res; + } + public static Adjustment CreateCharge(DateTime dt, int random) + { + var rnd = new DeterministicRandom(random); + Adjustment res = new Adjustment(); + res.currentDateTime = dt; + res.currentDescription = rnd.GetRandomItem(bills); + res.currentAmount = rnd.Random(10, 50) * 10; + return res; + } + + DateTime currentDateTime; + string currentDescription = ""; + double currentAmount = 0; + static readonly string[] bills = new string[] { "Bill - Insurance", "Bill - Electricity", "Bill - Rent", "Bill - Phone", "Bill - Office Supplies" }; + public DateTime Date { get { return currentDateTime; } } + public string Description { get { return currentDescription; } } + public double Amount { get { return currentAmount; } } + + public Adjustment() + { + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Customer.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Customer.cs new file mode 100644 index 00000000..d5199c91 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Customer.cs @@ -0,0 +1,64 @@ +using DevExpress.DataAccess.Sql; +using DevExpress.DataAccess.Sql.DataApi; + +namespace EnvelopeGenerator.Server.Client.Data { + public class Customer { + static List currentCustomers = new List(); + + public static List Customers { get { return currentCustomers; } } + static Customer() { + try { + SqlDataSource ds = new SqlDataSource("NWindConnectionString"); + SelectQuery query = SelectQueryFluentBuilder + .AddTable("Customers") + .SelectAllColumns() + .Build("Customers"); + ds.Queries.Add(query); + ds.RebuildResultSchema(); + ds.Fill(); + ITable src = ds.Result["Customers"]; + foreach(var row in src) { + currentCustomers.Add(new Customer() { + CustomerID = row.GetValue("CustomerID"), + Address = row.GetValue("Address"), + CompanyName = row.GetValue("CompanyName"), + ContactName = row.GetValue("ContactName"), + ContactTitle = row.GetValue("ContactTitle"), + Country = row.GetValue("Country"), + City = row.GetValue("City"), + Fax = row.GetValue("Fax"), + Phone = row.GetValue("Phone"), + PostalCode = row.GetValue("PostalCode"), + Region = row.GetValue("Region") + }); + } + } catch { + currentCustomers.Add(new Customer() { + Address = "Obere Str. 57", + City = "Berlin", + CompanyName = "Alfreds Futterkiste", + ContactName = "Maria Anders", + ContactTitle = "Sales Representative", + Country = "Germany", + CustomerID = "ALFKI", + Fax = "030-0076545", + Phone = "030-0074321", + PostalCode = "12209" + }); + } + } + + public string CustomerID { get; set; } + public string CompanyName { get; set; } + public string ContactName { get; set; } + public string ContactTitle { get; set; } + public string Address { get; set; } + public string City { get; set; } + public string PostalCode { get; set; } + public string Region { get; set; } + public string Country { get; set; } + public string Phone { get; set; } + public string Fax { get; set; } + + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DataItem.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DataItem.cs new file mode 100644 index 00000000..04c57f9d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DataItem.cs @@ -0,0 +1,71 @@ +namespace EnvelopeGenerator.Server.Client.Data { + public class DataItem { + static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" }; + public string CustomerID { get; set; } + public string CompanyName { get; set; } + public string ContactName { get; set; } + public string ContactTitle { get; set; } + public string Address { get; set; } + public string City { get; set; } + public string PostalCode { get; set; } + public string Region { get; set; } + public string Country { get; set; } + public string Phone { get; set; } + public string Fax { get; set; } + public string Email { get; set; } + public string Invoice { get; set; } + public string CustomerAccount { get; set; } + public string CustomerIdentifiers { get; set; } + public DateTime BillingDate { get; set; } + public DateTime BillingPeriodStart { get; set; } + public DateTime BillingPeriodEnd { get; set; } + public string Terms { get; set; } + public string TermsID { get; set; } + public Adjustment[] Adjustments { get; set; } + + public DataItem(int i) { + var rnd = new DeterministicRandom(i); + Customer c = rnd.GetRandomItem(Customer.Customers); + CustomerID = c.CustomerID; + CompanyName = c.CompanyName; + ContactName = c.ContactName; + ContactTitle = c.ContactTitle; + Address = c.Address; + City = c.City; + PostalCode = c.PostalCode; + Region = c.Region; + Country = c.Country; + Phone = c.Phone; + Fax = c.Fax; + Email = ContactName.Split(' ')[0].Replace(' ', '.').ToLower() + "@" + CompanyName.Split(' ')[0].ToLower() + ".com"; + Invoice = string.Format("{0}{1}-{2}", rnd.RandomChar, rnd.Random(100, 1000), rnd.Random(100, 1000)); + CustomerAccount = rnd.GetRandomItem(accountType); + CustomerIdentifiers = string.Format("{0}-{1}", rnd.Random(1000, 10000), rnd.Random(10, 100)); + BillingPeriodStart = rnd.RandomTime(); + BillingPeriodEnd = rnd.RandomTime(BillingPeriodStart, 7 * 24, 30 * 24); + BillingDate = rnd.RandomTime(BillingPeriodEnd, 7 * 24, 30 * 24); + Term currentTerm = rnd.GetRandomItem(Term.Terms); + Terms = currentTerm.Name; + + int adjustmentsCount = rnd.Random(6) + 4; + Adjustments = new Adjustment[adjustmentsCount]; + int h = (int)((BillingPeriodEnd - BillingPeriodStart).TotalHours / adjustmentsCount); + + Adjustments[0] = Adjustment.CreateBalanceForward(rnd.RandomTime(BillingPeriodStart, 0, h), rnd.Random(10000)); + + int[] items = rnd.RandomList(adjustmentsCount - 1, 2); + + for(int j = 1; j < Adjustments.Length; j++) { + DateTime nextDate = rnd.RandomTime(BillingPeriodStart.AddHours(h * j), 0, h); + switch(items[j - 1]) { + case 0: + Adjustments[j] = Adjustment.CreateCharge(nextDate, rnd.Random(10000)); + break; + case 1: + Adjustments[j] = Adjustment.CreatePayment(nextDate, rnd.Random(10000)); + break; + } + } + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DataItemList.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DataItemList.cs new file mode 100644 index 00000000..de62401b --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DataItemList.cs @@ -0,0 +1,70 @@ +using System.Collections; + +namespace EnvelopeGenerator.Server.Client.Data { + public class DataItemList : IList, IList { + readonly int rowCount; + + public DataItem this[int index] { get { return new DataItem(index); } set { } } + public int Count { get { return rowCount; } } + public bool IsReadOnly { get { return false; } } + public bool IsFixedSize { get { return false; } } + public object SyncRoot { get { return true; } } + public bool IsSynchronized { get { return true; } } + object IList.this[int index] { get { return new DataItem(index); } set { } } + + public DataItemList(int rowCount) { + this.rowCount = rowCount; + } + public IEnumerator GetEnumerator() { + throw new NotImplementedException(); + } + public int Add(object value) { + throw new NotImplementedException(); + } + public bool Contains(object value) { + throw new NotImplementedException(); + } + public void Clear() { + throw new NotImplementedException(); + } + public int IndexOf(object value) { + throw new NotImplementedException(); + } + public void Insert(int index, object value) { + throw new NotImplementedException(); + } + public void Remove(object value) { + throw new NotImplementedException(); + } + public void RemoveAt(int index) { + throw new NotImplementedException(); + } + public void CopyTo(Array array, int index) { + throw new NotImplementedException(); + } + IEnumerator IEnumerable.GetEnumerator() { + throw new NotImplementedException(); + } + public int IndexOf(DataItem item) { + throw new NotImplementedException(); + } + public void Insert(int index, DataItem item) { + throw new NotImplementedException(); + } + public void Add(DataItem item) { + throw new NotImplementedException(); + } + public bool Contains(DataItem item) { + throw new NotImplementedException(); + } + public void CopyTo(DataItem[] array, int arrayIndex) { + throw new NotImplementedException(); + } + public bool Remove(DataItem item) { + throw new NotImplementedException(); + } + void ICollection.CopyTo(DataItem[] array, int arrayIndex) { + CopyTo(array, arrayIndex); + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DeterministicRandom.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DeterministicRandom.cs new file mode 100644 index 00000000..aa02a78c --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/DeterministicRandom.cs @@ -0,0 +1,60 @@ +namespace EnvelopeGenerator.Server.Client.Data { + class DeterministicRandom { + const int randomCount = 10000; + static readonly int[] deterministicRandomNumbers; + static readonly DateTime time; + int rnd; + int Next { + get { + rnd = deterministicRandomNumbers[rnd % randomCount]; + return rnd; + } + } + public char RandomChar { + get { + return (char)((int)'A' + Random(0, 26)); + } + } + public int[] RandomList(int count, int to) { + int[] res = new int[count]; + for(int i = 0; i < Math.Min(count, to); i++) + res[i] = i; + for(int i = to; i < count; i++) + res[i] = Random(to); + + for(int i = 0; i < count; i++) { + int ind = Random(count); + int temp = res[ind]; + res[ind] = res[i]; + res[i] = temp; + } + return res; + } + public int Random(int to) { + return Random(0, to); + } + public int Random(int from, int to) { + return Next % Math.Max(1, to - from) + from; + } + public T GetRandomItem(IList list) { + return list[Next % list.Count]; + } + public DateTime RandomTime() { + return RandomTime(time, 0, 30 * 24); + } + public DateTime RandomTime(DateTime from, int fromHours, int toHours) { + return from.AddHours(Next % (toHours - fromHours) + fromHours); + } + + static DeterministicRandom() { + time = DateTime.Now.AddDays(-62); + Random currentRandom = new Random(randomCount); + deterministicRandomNumbers = new int[randomCount]; + for(int i = 0; i < randomCount; i++) + deterministicRandomNumbers[i] = currentRandom.Next(randomCount); + } + public DeterministicRandom(int i) { + this.rnd = i + (i >> 10) + (i >> 20); + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Term.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Term.cs new file mode 100644 index 00000000..d3590ae2 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Data/Term.cs @@ -0,0 +1,15 @@ +namespace EnvelopeGenerator.Server.Client.Data { + public struct Term { + public static readonly Term[] Terms = new Term[] { + new Term("Payment seven days after invoice date" ), + new Term("Payment ten days after invoice date" ), + new Term("End of month" ), + new Term("21st of the month following invoice date" ), + }; + readonly string currentName; + public string Name { get { return currentName; } } + public Term(string currentName) { + this.currentName = currentName; + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/EnvelopeGenerator.Server.Client.csproj b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/EnvelopeGenerator.Server.Client.csproj new file mode 100644 index 00000000..b00ba988 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/EnvelopeGenerator.Server.Client.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + true + Default + true + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + XtraReport + + + + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/Header.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/Header.razor new file mode 100644 index 00000000..9cb7c979 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/Header.razor @@ -0,0 +1,21 @@ + + +@code { + [Parameter] public bool ToggleOn { get; set; } + [Parameter] public EventCallback ToggleOnChanged { get; set; } + + async Task OnToggleClick() => await Toggle(); + + async Task Toggle(bool? value = null) { + var newValue = value ?? !ToggleOn; + if(ToggleOn != newValue) { + ToggleOn = newValue; + await ToggleOnChanged.InvokeAsync(ToggleOn); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor new file mode 100644 index 00000000..433431d7 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor @@ -0,0 +1,28 @@ +@using EnvelopeGenerator.Server.Client.Services; +@inherits LayoutComponentBase + +
+
+
+ @Body +
+
+ +
+ +@code { + [Inject] IHttpClientFactory HttpClientFactory { get; set; } = default!; + + List RequiredFonts = new() { + "opensans.ttf" + }; + + protected async override Task OnInitializedAsync() { + await FontLoader.LoadFonts(HttpClientFactory, RequiredFonts); + await base.OnInitializedAsync(); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor.css new file mode 100644 index 00000000..df8c10ff --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/MainLayout.razor.css @@ -0,0 +1,18 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/NavMenu.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/NavMenu.razor new file mode 100644 index 00000000..fa17f626 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Layout/NavMenu.razor @@ -0,0 +1,46 @@ + + +
+ +
+ +@code { + private bool collapseNavMenu = true; + + private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/AnnotationDto.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/AnnotationDto.cs new file mode 100644 index 00000000..e87ad91f --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/AnnotationDto.cs @@ -0,0 +1,32 @@ +namespace EnvelopeGenerator.Server.Client.Models; + +/// +/// Represents a pre-assigned signature annotation position on a specific page. +///

+/// Coordinate unit (X, Y): Inches (GdPicture14 native unit), +/// origin at the top-left corner of the page, both axes increase downward/rightward. +///

+/// Conversion to DevExpress: Multiply by 100 (DX uses 1/100 inch). +/// Convert: xDX = xInches * 100.0 +///
+/// Conversion to PDF Points: Multiply by 72 (1 inch = 72 points). +/// Convert: xPt = xInches * 72.0 +///
+/// Y-axis for PDF (bottom-left origin): Flip required for iText7. +/// Convert: yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0 +///
+[Obsolete("Use SignatureDto with SignatureService.")] +public record AnnotationDto +{ + /// Unique identifier of the annotation. + public long Id { get; init; } + + /// 1-based page number within the document. + public int Page { get; init; } + + /// Horizontal position in INCHES from the left edge of the page. + public double X { get; init; } + + /// Vertical position in INCHES from the top edge of the page. + public double Y { get; init; } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/Constants/SenderAppType.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/Constants/SenderAppType.cs new file mode 100644 index 00000000..ae142bcf --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/Constants/SenderAppType.cs @@ -0,0 +1,8 @@ +namespace EnvelopeGenerator.Server.Client.Models.Constants +{ + public enum SenderAppType + { + LegacyFormApp = 0, + ReceiverUIBlazorApp = 1 + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/Constants/UnitOfLength.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/Constants/UnitOfLength.cs new file mode 100644 index 00000000..4741db8e --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/Constants/UnitOfLength.cs @@ -0,0 +1,65 @@ +namespace EnvelopeGenerator.Server.Client.Models.Constants; + +/// +/// Represents the unit of measurement for coordinate values in signature positioning. +/// Used for converting coordinates between different systems (GdPicture14, PDF.js, iText7). +/// +public enum UnitOfLength +{ + /// + /// Inch unit (1 inch = 25.4 mm). + /// This is the native unit used by GdPicture14 (EnvelopeGenerator.Form - Legacy VB.NET app). + /// Database stores all coordinates (X, Y, Width, Height) in INCHES. + /// + /// + /// Source: GdPicture14.Annotations.AnnotationStickyNote uses INCHES natively. + ///
+ /// Evidence: VB.NET code directly assigns database values to annotation properties without conversion: + /// + /// oAnnotation.Left = CSng(pElement.X) ' Direct assignment → INCHES + /// oAnnotation.Top = CSng(pElement.Y) + /// + /// Standard Page Dimensions: + /// + /// A4: 8.27" × 11.69" (210mm × 297mm) + /// Letter: 8.5" × 11" + /// + ///
+ Inch = 0, + + /// + /// PDF Point unit (1 point = 1/72 inch). + /// This is the standard unit used by PDF specification and PDF.js viewer. + /// + /// + /// Definition: According to PDF specification and Microsoft documentation: + ///
+ /// "PDF pages are sized in point units. 1 pt == 1/72 inch" + ///

+ /// Conversion Formula: + /// + /// points = inches * 72.0 + /// inches = points / 72.0 + /// + /// Important: Point ≠ Pixel! + /// + /// Point (pt): Device-independent unit (always 1/72 inch) + /// Pixel (px): Device-dependent unit (varies with screen DPI) + /// At 72 DPI: 1 point = 1 pixel (coincidence) + /// At 96 DPI: 1 point ≈ 1.33 pixels + /// At 300 DPI: 1 point ≈ 4.17 pixels + /// + /// Standard Page Dimensions (in points): + /// + /// A4: 595 × 842 points (8.27" × 11.69" × 72) + /// Letter: 612 × 792 points (8.5" × 11" × 72) + /// + /// Usage in EnvelopeGenerator: + /// + /// PDF.js viewer expects coordinates in points + /// iText7 library uses points for PDF manipulation + /// PSPDFKit (Web) uses points for annotation placement + /// + ///
+ Point +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/EnvelopeReceiverDto.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/EnvelopeReceiverDto.cs new file mode 100644 index 00000000..f99e8587 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/EnvelopeReceiverDto.cs @@ -0,0 +1,105 @@ +namespace EnvelopeGenerator.Server.Client.Models; + +/// +/// Client-side model for the envelope receiver returned by +/// GET api/EnvelopeReceiver/{envelopeKey}. +/// +public record EnvelopeReceiverDto +{ + public int EnvelopeId { get; init; } + public int ReceiverId { get; init; } + public int Sequence { get; init; } + + public string? Name { get; init; } + public string? JobTitle { get; init; } + public string? CompanyName { get; init; } + public string? PrivateMessage { get; init; } + + public DateTime AddedWhen { get; init; } + public DateTime? ChangedWhen { get; init; } + public bool HasPhoneNumber { get; init; } + + public EnvelopeClientDto? Envelope { get; init; } + public ReceiverClientDto? Receiver { get; init; } +} + +/// +/// Client-side model for the envelope data embedded in . +/// +public record EnvelopeClientDto +{ + public int Id { get; init; } + public int UserId { get; init; } + public int Status { get; init; } + public string StatusName { get; init; } = string.Empty; + public string Uuid { get; init; } = string.Empty; + public string Title { get; init; } = string.Empty; + public string Message { get; init; } = string.Empty; + public DateTime AddedWhen { get; init; } + public DateTime? ChangedWhen { get; init; } + public string Language { get; init; } = "de-DE"; + public int? EnvelopeTypeId { get; init; } + public string? EnvelopeTypeTitle { get; init; } + public int? ContractType { get; init; } + public int? CertificationType { get; init; } + public bool UseAccessCode { get; init; } + public bool TFAEnabled { get; init; } + public IEnumerable? Documents { get; init; } + public EnvelopeSenderDto? User { get; init; } +} + +/// +/// Sender (user) information embedded in . +/// +public record EnvelopeSenderDto +{ + public int Id { get; init; } + public string? Username { get; init; } + public string? FullName { get; init; } + public string? Email { get; init; } +} + +/// +/// Client-side model for a document embedded in . +/// +public record DocumentClientDto +{ + public int Id { get; init; } + public int EnvelopeId { get; init; } + public DateTime AddedWhen { get; init; } + public IEnumerable? Elements { get; init; } +} + +/// +/// Client-side model for a signature/annotation element embedded in . +/// +public record SignatureClientDto +{ + public int Id { get; init; } + public int DocumentId { get; init; } + public int ReceiverId { get; init; } + public int ElementType { get; init; } + public double X { get; init; } + public double Y { get; init; } + public double Width { get; init; } + public double Height { get; init; } + public int Page { get; init; } + public bool Required { get; init; } + public string? Tooltip { get; init; } + public bool ReadOnly { get; init; } + public int AnnotationIndex { get; init; } + public DateTime AddedWhen { get; init; } + public DateTime? ChangedWhen { get; init; } +} + +/// +/// Client-side model for the receiver data embedded in . +/// +public record ReceiverClientDto +{ + public int Id { get; init; } + public string? EmailAddress { get; init; } + public string? Signature { get; init; } + public DateTime AddedWhen { get; init; } + public DateTime? TfaRegDeadline { get; init; } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs new file mode 100644 index 00000000..9a69f24f --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs @@ -0,0 +1,62 @@ +namespace EnvelopeGenerator.Server.Client.Models; + +/// +/// Represents a captured signature with metadata created by the receiver in the signature popup. +/// This model holds the signature image (as base64 data URL) along with signer information +/// used for rendering applied signatures on the PDF canvas. +/// +/// +/// Used in: EnvelopeViewer.razor signature popup workflow +///
+/// Creation: User draws/types/uploads signature and fills required fields +///
+/// Storage: Session-only (Blazor component state, lost on page refresh) +///
+/// Rendering: Applied signatures display: Image + Separator + Name/Position/Place/Date +///
+public sealed record SignatureCaptureDto +{ + /// + /// Base64-encoded data URL of the signature image. + ///
+ /// Format: data:image/png;base64,iVBORw0KG... + ///
+ /// Source: Canvas.toDataURL() from signature pad (draw/text/image tabs) + ///
+ /// Usage: Set as img.src in applied signature overlay + ///
+ public required string DataUrl { get; init; } + + /// + /// Full name of the signer (first and last name). + ///
+ /// Required: Yes (validated in popup) + ///
+ /// Display: Bold text in applied signature block + ///
+ /// Example: "Max Mustermann" + ///
+ public required string FullName { get; init; } + + /// + /// Job title or position of the signer. + ///
+ /// Required: No (optional field) + ///
+ /// Display: Normal weight text between name and place/date + ///
+ /// Example: "Geschftsfhrer" or empty string + ///
+ public string Position { get; init; } = string.Empty; + + /// + /// Location/place where the signature was created. + ///
+ /// Required: Yes (validated in popup) + ///
+ /// Display: Shown with current date in German format (dd.MM.yyyy) + ///
+ /// Example: "Berlin" ? rendered as "Berlin, 26.01.2025" + ///
+ public required string Place { get; init; } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureDto.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureDto.cs new file mode 100644 index 00000000..4713ab80 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Models/SignatureDto.cs @@ -0,0 +1,101 @@ +using EnvelopeGenerator.Server.Client.Models.Constants; + +namespace EnvelopeGenerator.Server.Client.Models; + +/// +/// Represents a signature position on a PDF page. +/// Coordinates stored in INCHES (GdPicture14 native unit). +/// Origin: Top-left corner, X increases right, Y increases down. +/// +public class SignatureDto +{ + /// Unique identifier. + public int Id { get; init; } + + private double _x; + private double _y; + + /// Horizontal position in INCHES from left edge. + public double X + { + get => _x * Factor; + init => _x = value; + } + + /// Vertical position in INCHES from top edge. + public double Y + { + get => _y * Factor; + init => _y = value; + } + + /// 1-based page number. + public int Page { get; init; } + + /// Sender application type that created this signature. + public SenderAppType SenderAppType { get; init; } + + private UnitOfLength _unitOfLength; + + public SignatureDto Convert(UnitOfLength unitOfLength) + { + _unitOfLength = unitOfLength; + return this; + } + + public double Factor + { + get + { + if (SenderAppType != SenderAppType.LegacyFormApp) + { + throw new NotImplementedException( + $"SenderAppType '{SenderAppType}' is not yet implemented. " + + $"Currently, only '{nameof(SenderAppType.LegacyFormApp)}' is supported. " + + $"Future implementations will handle '{nameof(SenderAppType.ReceiverUIBlazorApp)}' and other types."); + } + + // LegacyFormApp uses GdPicture14 with INCHES + return _unitOfLength switch + { + UnitOfLength.Inch => 1.0, // No conversion needed: INCHES → INCHES + UnitOfLength.Point => 72.0, // INCHES → PDF Points: 1 inch = 72 points (PDF standard, NOT pixels!) + _ => throw new InvalidOperationException( + $"Unknown UnitOfLength: {_unitOfLength}. Expected '{nameof(UnitOfLength.Inch)}' or '{nameof(UnitOfLength.Point)}'.") + }; + } + } +} + +public static class SignatureDtoExtensions +{ + /// + /// Converts all signatures in the collection to the specified unit of length. + /// + /// Type of the collection (IEnumerable, List, etc.) + /// Collection of SignatureDto objects to convert. + /// Target unit of measurement (Inch or Point). + /// The same collection with all signatures converted to the specified unit. + /// Thrown when signatures collection is null. + /// + /// Usage: + /// + /// var signatures = await SignatureService.GetAsync(envelopeKey); + /// var convertedSignatures = signatures.ConvertAll(UnitOfLength.Point); + /// + /// Note: This method modifies each SignatureDto object in place and returns the same collection. + /// + public static T Convert(this T signatures, UnitOfLength unitOfLength) + where T : IEnumerable + { + if (signatures == null) + throw new ArgumentNullException(nameof(signatures)); + + foreach (var signature in signatures) + { + signature.Convert(unitOfLength); + } + + return signatures; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Options/ApiOptions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Options/ApiOptions.cs new file mode 100644 index 00000000..a3a88c9b --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Options/ApiOptions.cs @@ -0,0 +1,8 @@ +namespace EnvelopeGenerator.Server.Client.Options; + +public class ApiOptions +{ + public const string SectionName = "Api"; + + public bool UsePredefinedReports { get; set; } = false; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Options/PdfViewerOptions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Options/PdfViewerOptions.cs new file mode 100644 index 00000000..9c1e2da7 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Options/PdfViewerOptions.cs @@ -0,0 +1,71 @@ +namespace EnvelopeGenerator.Server.Client.Options; + +public class PdfViewerOptions +{ + public const string SectionName = "PdfViewer"; + + /// + /// Base scale for thumbnail rendering (0.2 - 1.5 recommended) + /// Higher values = better quality but slower rendering + /// Default: 0.75 + /// + public double ThumbnailBaseScale { get; set; } = 0.75; + + /// + /// Enable HiDPI/Retina support for thumbnails + /// Default: true + /// + public bool ThumbnailEnableHiDPI { get; set; } = true; + + /// + /// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0) + /// Caps DPR to avoid excessive memory usage on 4K+ displays + /// Default: 2.0 + /// + public double ThumbnailMaxDPR { get; set; } = 2.0; + + /// + /// Enable HiDPI/Retina support for main PDF canvas + /// Default: true + /// + public bool MainCanvasEnableHiDPI { get; set; } = true; + + /// + /// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0) + /// Default: 2.0 + /// + public double MainCanvasMaxDPR { get; set; } = 2.0; + + /// + /// Enable smooth zoom transition (fade effect) + /// Default: true + /// + public bool EnableSmoothZoom { get; set; } = true; + + /// + /// Zoom transition duration in milliseconds (50 - 500) + /// Default: 150 + /// + public int ZoomTransitionDuration { get; set; } = 150; + + /// + /// Opacity during rendering (0.0 - 1.0) + /// Lower values = more visible fade effect + /// Default: 0.85 + /// + public double RenderingOpacity { get; set; } = 0.85; + + /// + /// Delay between thumbnail renders in milliseconds (10 - 200) + /// Higher values = less browser stress, slower initial load + /// Default: 50 + /// + public int ThumbnailRenderDelay { get; set; } = 50; + + /// + /// Zoom step percentage (1 - 50) + /// Controls how much zoom changes per click or scroll + /// Default: 5 (5% per step) + /// + public int ZoomStepPercentage { get; set; } = 5; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor new file mode 100644 index 00000000..2936a900 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor @@ -0,0 +1,449 @@ +@page "/sender" +@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)) +@using System.Text.Json +@using EnvelopeGenerator.Domain.Constants +@using EnvelopeGenerator.Server.Client.Models +@using DevExpress.Blazor +@using EnvelopeGenerator.Server.Client.Services +@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@inject AppVersionService AppVersion +@using EnvelopeGenerator.Application.Common.Dto +@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@inject AppVersionService AppVersion + + + + + +
+
+
+
+ +
Umschlag-Übersicht
+
+ +
+ + + + + + + + + +
+
+
+ +
+ @if (_isLoading && _allEnvelopes == null) { +
+
+
+ Lädt... +
+

Umschläge werden geladen...

+
+
+ } else if (_errorMessage != null) { +
+
+
+ + + + +
+
Fehler beim Laden der Umschläge
+

@_errorMessage

+
+
+
+
+ } else { +
+
+ + +
+ +
+ @if (_activeTab == "active") { + + + + + @((cellContext.DataItem as EnvelopeDto)?.Id) + + + + + @((cellContext.DataItem as EnvelopeDto)?.Title) + + + + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var statusInfo = GetStatusInfo(envelope.Status); +
+ + @statusInfo.Label +
+ } + } +
+
+ + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var receivers = envelope.EnvelopeReceivers?.ToList() ?? []; + var signed = receivers.Count(r => r.Signed); + var total = receivers.Count; +
+ + @signed / @total unterschrieben + + @if (total > 0) { +
+
+
+ } +
+ } + } +
+
+
+ +
+
Empfänger
+ @{ + var envelope = detailContext.DataItem as EnvelopeDto; + if (envelope?.EnvelopeReceivers?.Any() == true) { +
+ @foreach (var receiver in envelope.EnvelopeReceivers) { +
+ + @if (receiver.Signed) { + + + + Unterschrieben + } else { + + + + + Ausstehend + } + +
+ @receiver.Name + @receiver.Receiver?.EmailAddress +
+
+ } +
+ } else { +

Keine Empfänger

+ } + } +
+
+
+ } else { + + + + + @((cellContext.DataItem as EnvelopeDto)?.Id) + + + + + @((cellContext.DataItem as EnvelopeDto)?.Title) + + + + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var statusInfo = GetStatusInfo(envelope.Status); +
+ + @statusInfo.Label +
+ } + } +
+
+ + + @{ + var envelope = cellContext.DataItem as EnvelopeDto; + if (envelope != null) { + var receivers = envelope.EnvelopeReceivers?.ToList() ?? []; + var signed = receivers.Count(r => r.Signed); + var total = receivers.Count; +
+ + @signed / @total unterschrieben + + @if (total > 0) { +
+
+
+ } +
+ } + } +
+
+
+ +
+
Empfänger
+ @{ + var envelope = detailContext.DataItem as EnvelopeDto; + if (envelope?.EnvelopeReceivers?.Any() == true) { +
+ @foreach (var receiver in envelope.EnvelopeReceivers) { +
+ + @if (receiver.Signed) { + + + + Unterschrieben + } else { + + + + + Ausstehend + } + +
+ @receiver.Name + @receiver.Receiver?.EmailAddress +
+
+ } +
+ } else { +

Keine Empfänger

+ } + } +
+
+
+ } +
+
+ } +
+
+ +@code { + private IEnumerable? _allEnvelopes; + private IEnumerable? _activeEnvelopes; + private IEnumerable? _completedEnvelopes; + private EnvelopeDto? _selectedEnvelope; + private string _activeTab = "active"; + private bool _isLoading = true; + private bool _isLoggingOut = false; + private string? _errorMessage; + private DxGrid? _gridActive; + private DxGrid? _gridCompleted; + + protected override async Task OnInitializedAsync() + { + var hasAccess = await AuthService.CheckSenderAccessAsync(); + if (!hasAccess) + { + Navigation.NavigateTo($"/sender/login"); + return; + } + + await LoadEnvelopesAsync(); + } + + async Task LoadEnvelopesAsync() + { + _isLoading = true; + _errorMessage = null; + await InvokeAsync(StateHasChanged); + + try + { + _allEnvelopes = await EnvelopeService.GetAsync() ?? []; + + // Split into active and completed based on status + var envelopes = _allEnvelopes.ToList(); + _activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList(); + _completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList(); + + await JSRuntime.InvokeVoidAsync("console.log", $"Loaded {_activeEnvelopes.Count()} active and {_completedEnvelopes.Count()} completed envelopes"); + } + catch (Exception ex) + { + _errorMessage = ex.Message; + await JSRuntime.InvokeVoidAsync("console.error", "Fehler beim Laden der Umschläge:", ex.ToString()); + } + finally + { + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + } + + async Task RefreshEnvelopes() + { + await LoadEnvelopesAsync(); + } + + void CreateEnvelope() + { + Navigation.NavigateTo("/sender/editor"); + } + + void EditEnvelope() + { + if (_selectedEnvelope == null) return; + // TODO: Navigate to envelope editor + JSRuntime.InvokeVoidAsync("console.log", $"Edit envelope {_selectedEnvelope.Id} clicked - not yet implemented"); + } + + void DeleteEnvelope() + { + if (_selectedEnvelope == null) return; + // TODO: Show delete confirmation dialog + JSRuntime.InvokeVoidAsync("console.log", $"Delete envelope {_selectedEnvelope.Id} clicked - not yet implemented"); + } + + async Task LogoutAsync() + { + _isLoggingOut = true; + await InvokeAsync(StateHasChanged); + await AuthService.LogoutSenderAsync(); + Navigation.NavigateTo("/sender/login", forceLoad: true); + } + + bool IsEnvelopeSent(EnvelopeDto envelope) + { + var status = (EnvelopeStatus)envelope.Status; + return status >= EnvelopeStatus.EnvelopeQueued; + } + + (string Label, string CssClass, string DotColor) GetStatusInfo(EnvelopeStatus status) + { + return status switch + { + EnvelopeStatus.EnvelopePartlySigned => ("Teilweise unterschrieben", "partly-signed", "green"), + EnvelopeStatus.EnvelopeQueued => ("In Warteschlange", "queued", "orange"), + EnvelopeStatus.EnvelopeSent => ("Gesendet", "sent", "orange"), + EnvelopeStatus.EnvelopeCompletelySigned => ("Vollständig unterschrieben", "completed", "green"), + EnvelopeStatus.EnvelopeDeleted => ("Gelöscht", "deleted", "red"), + EnvelopeStatus.EnvelopeRejected => ("Abgelehnt", "rejected", "red"), + EnvelopeStatus.EnvelopeWithdrawn => ("Zurückgezogen", "withdrawn", "red"), + EnvelopeStatus.EnvelopeCreated => ("Erstellt", "created", "blue"), + EnvelopeStatus.EnvelopeSaved => ("Gespeichert", "saved", "blue"), + _ => ("Unbekannt", "unknown", "blue") + }; + } + + void OnCustomizeElement(GridCustomizeElementEventArgs e) + { + // Future: Add custom row coloring based on status if needed + } + + void OnSelectedEnvelopeChanged(object envelope) + { + _selectedEnvelope = envelope as EnvelopeDto; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor new file mode 100644 index 00000000..c8d170cb --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor @@ -0,0 +1,85 @@ +@page "/" +@inject IJSRuntime JS +@inject NavigationManager Navigation +@rendermode InteractiveWebAssembly + + + +
+ +
+
+ + + +
+

SignFlow

+

Willkommen im eSign-Portal

+
+
+
+ +
+
+
+ +

+ +

+ +
+ + + +
+
+ + + + Sicherer Zugang +
+
+ + + + Digitale Unterschrift +
+
+ + + + + PDF-Export +
+
+ +
+ +
+
+
+ +
+ +@code { + private const string HomePageDescription = + "Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. " + + "Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " + + "Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente."; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15); + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor new file mode 100644 index 00000000..de390551 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor @@ -0,0 +1,173 @@ +@page "/envelope/login/{EnvelopeKey}" +@rendermode InteractiveWebAssembly +@using EnvelopeGenerator.Server.Client.Services +@inject AuthService AuthService +@inject NavigationManager Navigation + + + + + +@code { + [Parameter] public string EnvelopeKey { get; set; } = string.Empty; + + string AccessCode = string.Empty; + bool ShowCode; + bool IsLoading; + EnvelopeLoginResult? LoginResult; + + async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) + { + if (e.Key == "Enter") + await SubmitAsync(); + } + + async Task SubmitAsync() + { + if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return; + + IsLoading = true; + LoginResult = null; + await InvokeAsync(StateHasChanged); + + var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim()); + + if (result == EnvelopeLoginResult.Success) + { + Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); + return; + } + + LoginResult = result; + IsLoading = false; + await InvokeAsync(StateHasChanged); + } +} + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor new file mode 100644 index 00000000..32d74e97 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor @@ -0,0 +1,172 @@ +@page "/sender/login" +@rendermode InteractiveWebAssembly +@using EnvelopeGenerator.Server.Client.Services +@inject AuthService AuthService +@inject NavigationManager Navigation + + + + + +@code { + string Username = string.Empty; + string Password = string.Empty; + bool ShowPassword; + bool IsLoading; + SenderLoginResult? LoginResult; + + async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) { + if (e.Key == "Enter") + await SubmitAsync(); + } + + async Task SubmitAsync() { + if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password) || IsLoading) return; + + IsLoading = true; + LoginResult = null; + await InvokeAsync(StateHasChanged); + + var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim()); + + if (result == SenderLoginResult.Success) { + Navigation.NavigateTo("/sender", forceLoad: true); + return; + } + + LoginResult = result; + IsLoading = false; + await InvokeAsync(StateHasChanged); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/Report.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/Report.cs new file mode 100644 index 00000000..83333baa --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/Report.cs @@ -0,0 +1,1198 @@ +using DevExpress.XtraReports.UI; +namespace EnvelopeGenerator.Server.Client.PredefinedReports { + public class Report : XtraReport { + private TopMarginBand topMarginBand1; + private XRPageInfo xrPageInfo4; + private XRPageInfo xrPageInfo3; + private BottomMarginBand bottomMarginBand1; + private DetailBand detailBand1; + private XRLabel xrLabel1; + private XRBarCode xrBarCode1; + private XRLabel xrLabel4; + private XRLabel xrLabel5; + private XRLabel xrLabel6; + private XRLabel xrLabel7; + private XRLabel xrLabel8; + private XRLabel xrLabel9; + private XRLabel xrLabel10; + private XRLabel xrLabel11; + private XRLabel xrLabel12; + private XRLabel xrLabel13; + private SubBand SubBand1; + private XRTable xrTable5; + private XRTableRow xrTableRow9; + private XRTableCell xrTableCell18; + private XRTableCell xrTableCell20; + private XRTableRow xrTableRow11; + private XRTableCell xrTableCell24; + private XRTableCell xrTableCell25; + private XRTableRow xrTableRow12; + private XRTableCell xrTableCell26; + private XRTableCell xrTableCell27; + private XRTableRow xrTableRow10; + private XRTableCell xrTableCell21; + private XRTableCell xrTableCell23; + private XRTableRow xrTableRow13; + private XRTableCell xrTableCell28; + private XRTableCell xrTableCell29; + private XRLabel xrLabel2; + private DetailReportBand detailReportBand1; + private GroupHeaderBand groupHeaderBand1; + private XRTable xrTable2; + private XRTableRow xrTableRow3; + private XRTableCell xrTableCell7; + private XRTableCell xrTableCell8; + private XRTableCell xrTableCell9; + private XRTableCell xrTableCell11; + private DetailBand detailBand2; + private XRTable xrTable3; + private XRTableRow xrTableRow4; + private XRTableCell xrTableCell12; + private XRTableCell xrTableCell13; + private XRTableCell xrTableCell15; + private XRTableCell xrTableCell16; + private ReportHeaderBand ReportHeader; + private XRLabel xrLabel3; + private ReportFooterBand ReportFooter; + private XRTable xrTable1; + private XRTableRow xrTableRow1; + private XRTableCell xrTableCell3; + private XRTableCell xrTableCell1; + private XRTableCell xrTableCell2; + private XRTableRow xrTableRow2; + private XRTableCell xrTableCell6; + private XRTableCell xrTableCell4; + private XRTableCell xrTableCell5; + private DevExpress.DataAccess.ObjectBinding.ObjectDataSource objectDataSource1; + private XRControlStyle Title; + private XRControlStyle ReportTitleCaption; + private XRControlStyle DetailData3; + private XRControlStyle DetailData3_Odd; + private XRControlStyle PageInfo; + private XRControlStyle Headers; + private XRControlStyle SummaryTitles; + private XRControlStyle SummaryValues; + protected DevExpress.XtraReports.Parameters.Parameter RowCountParameter; + private System.ComponentModel.IContainer components; + + public Report() : this(35u) { } + + public Report(uint countParameter) { + InitializeComponent(); + RowCountParameter.Value = countParameter; + Name = "LargeDatasetReport"; + DisplayName = "Large Dataset"; + } + + private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); + DevExpress.XtraPrinting.BarCode.QRCodeGenerator qrCodeGenerator1 = new DevExpress.XtraPrinting.BarCode.QRCodeGenerator(); + DevExpress.XtraReports.UI.XRSummary xrSummary1 = new DevExpress.XtraReports.UI.XRSummary(); + DevExpress.DataAccess.ObjectBinding.ObjectConstructorInfo objectConstructorInfo1 = new DevExpress.DataAccess.ObjectBinding.ObjectConstructorInfo(); + DevExpress.DataAccess.ObjectBinding.Parameter parameter1 = new DevExpress.DataAccess.ObjectBinding.Parameter(); + this.topMarginBand1 = new DevExpress.XtraReports.UI.TopMarginBand(); + this.bottomMarginBand1 = new DevExpress.XtraReports.UI.BottomMarginBand(); + this.detailBand1 = new DevExpress.XtraReports.UI.DetailBand(); + this.detailReportBand1 = new DevExpress.XtraReports.UI.DetailReportBand(); + this.xrPageInfo4 = new DevExpress.XtraReports.UI.XRPageInfo(); + this.xrPageInfo3 = new DevExpress.XtraReports.UI.XRPageInfo(); + this.xrLabel1 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrBarCode1 = new DevExpress.XtraReports.UI.XRBarCode(); + this.xrLabel4 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel5 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel6 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel7 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel8 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel9 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel10 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel11 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel12 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrLabel13 = new DevExpress.XtraReports.UI.XRLabel(); + this.SubBand1 = new DevExpress.XtraReports.UI.SubBand(); + this.xrTable5 = new DevExpress.XtraReports.UI.XRTable(); + this.xrLabel2 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrTableRow9 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableRow11 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableRow12 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableRow10 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableRow13 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableCell18 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell20 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell24 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell25 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell26 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell27 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell21 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell23 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell28 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell29 = new DevExpress.XtraReports.UI.XRTableCell(); + this.groupHeaderBand1 = new DevExpress.XtraReports.UI.GroupHeaderBand(); + this.detailBand2 = new DevExpress.XtraReports.UI.DetailBand(); + this.ReportHeader = new DevExpress.XtraReports.UI.ReportHeaderBand(); + this.ReportFooter = new DevExpress.XtraReports.UI.ReportFooterBand(); + this.xrTable2 = new DevExpress.XtraReports.UI.XRTable(); + this.xrTableRow3 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableCell7 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell8 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell9 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell11 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTable3 = new DevExpress.XtraReports.UI.XRTable(); + this.xrTableRow4 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableCell12 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell13 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell15 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell16 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrLabel3 = new DevExpress.XtraReports.UI.XRLabel(); + this.xrTable1 = new DevExpress.XtraReports.UI.XRTable(); + this.xrTableRow1 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableRow2 = new DevExpress.XtraReports.UI.XRTableRow(); + this.xrTableCell3 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell1 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell2 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell6 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell4 = new DevExpress.XtraReports.UI.XRTableCell(); + this.xrTableCell5 = new DevExpress.XtraReports.UI.XRTableCell(); + this.objectDataSource1 = new DevExpress.DataAccess.ObjectBinding.ObjectDataSource(this.components); + this.Title = new DevExpress.XtraReports.UI.XRControlStyle(); + this.ReportTitleCaption = new DevExpress.XtraReports.UI.XRControlStyle(); + this.DetailData3 = new DevExpress.XtraReports.UI.XRControlStyle(); + this.DetailData3_Odd = new DevExpress.XtraReports.UI.XRControlStyle(); + this.PageInfo = new DevExpress.XtraReports.UI.XRControlStyle(); + this.Headers = new DevExpress.XtraReports.UI.XRControlStyle(); + this.SummaryTitles = new DevExpress.XtraReports.UI.XRControlStyle(); + this.SummaryValues = new DevExpress.XtraReports.UI.XRControlStyle(); + this.RowCountParameter = new DevExpress.XtraReports.Parameters.Parameter(); + ((System.ComponentModel.ISupportInitialize)(this.xrTable5)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.xrTable2)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.xrTable3)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.xrTable1)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.objectDataSource1)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this)).BeginInit(); + // + // topMarginBand1 + // + this.topMarginBand1.Controls.AddRange(new DevExpress.XtraReports.UI.XRControl[] { + this.xrPageInfo4, + this.xrPageInfo3}); + this.topMarginBand1.Name = "topMarginBand1"; + // + // bottomMarginBand1 + // + this.bottomMarginBand1.Name = "bottomMarginBand1"; + // + // detailBand1 + // + this.detailBand1.Controls.AddRange(new DevExpress.XtraReports.UI.XRControl[] { + this.xrLabel1, + this.xrBarCode1, + this.xrLabel4, + this.xrLabel5, + this.xrLabel6, + this.xrLabel7, + this.xrLabel8, + this.xrLabel9, + this.xrLabel10, + this.xrLabel11, + this.xrLabel12, + this.xrLabel13}); + this.detailBand1.HeightF = 197.9167F; + this.detailBand1.KeepTogether = true; + this.detailBand1.Name = "detailBand1"; + this.detailBand1.SubBands.AddRange(new DevExpress.XtraReports.UI.SubBand[] { + this.SubBand1}); + // + // detailReportBand1 + // + this.detailReportBand1.Bands.AddRange(new DevExpress.XtraReports.UI.Band[] { + this.groupHeaderBand1, + this.detailBand2, + this.ReportHeader, + this.ReportFooter}); + this.detailReportBand1.DataMember = "Adjustments"; + this.detailReportBand1.DataSource = this.objectDataSource1; + this.detailReportBand1.Level = 0; + this.detailReportBand1.Name = "detailReportBand1"; + this.detailReportBand1.PageBreak = DevExpress.XtraReports.UI.PageBreak.AfterBand; + // + // xrPageInfo4 + // + this.xrPageInfo4.LocationFloat = new DevExpress.Utils.PointFloat(461.75F, 38.73147F); + this.xrPageInfo4.Name = "xrPageInfo4"; + this.xrPageInfo4.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 0, 0, 0, 100F); + this.xrPageInfo4.PageInfo = DevExpress.XtraPrinting.PageInfo.DateTime; + this.xrPageInfo4.SizeF = new System.Drawing.SizeF(178.875F, 23F); + this.xrPageInfo4.StyleName = "PageInfo"; + this.xrPageInfo4.StylePriority.UseForeColor = false; + this.xrPageInfo4.StylePriority.UsePadding = false; + this.xrPageInfo4.StylePriority.UseTextAlignment = false; + this.xrPageInfo4.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + this.xrPageInfo4.TextFormatString = "Issued: {0}"; + // + // xrPageInfo3 + // + this.xrPageInfo3.LocationFloat = new DevExpress.Utils.PointFloat(0F, 38.73146F); + this.xrPageInfo3.Name = "xrPageInfo3"; + this.xrPageInfo3.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 2, 0, 0, 100F); + this.xrPageInfo3.SizeF = new System.Drawing.SizeF(300.2737F, 23F); + this.xrPageInfo3.StyleName = "PageInfo"; + this.xrPageInfo3.StylePriority.UseForeColor = false; + this.xrPageInfo3.StylePriority.UsePadding = false; + this.xrPageInfo3.StylePriority.UseTextAlignment = false; + this.xrPageInfo3.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + this.xrPageInfo3.TextFormatString = "Page {0} of {1} Pages"; + // + // xrLabel1 + // + this.xrLabel1.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[CompanyName]")}); + this.xrLabel1.LocationFloat = new DevExpress.Utils.PointFloat(0F, 0F); + this.xrLabel1.Name = "xrLabel1"; + this.xrLabel1.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 2, 0, 0, 100F); + this.xrLabel1.SizeF = new System.Drawing.SizeF(650F, 50F); + this.xrLabel1.StyleName = "ReportTitleCaption"; + this.xrLabel1.StylePriority.UseFont = false; + this.xrLabel1.StylePriority.UseForeColor = false; + this.xrLabel1.StylePriority.UsePadding = false; + this.xrLabel1.Text = "Paris spécialités"; + // + // xrBarCode1 + // + this.xrBarCode1.Alignment = DevExpress.XtraPrinting.TextAlignment.MiddleCenter; + this.xrBarCode1.AutoModule = true; + this.xrBarCode1.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Address] + NewLine() + [City] + \' , \' + [PostalCode] + NewLine() + [Phone]")}); + this.xrBarCode1.LocationFloat = new DevExpress.Utils.PointFloat(514.9792F, 50.18913F); + this.xrBarCode1.Module = 4.7F; + this.xrBarCode1.Name = "xrBarCode1"; + this.xrBarCode1.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 0, 0, 0, 100F); + this.xrBarCode1.ShowText = false; + this.xrBarCode1.SizeF = new System.Drawing.SizeF(135.021F, 124.9819F); + this.xrBarCode1.StylePriority.UsePadding = false; + qrCodeGenerator1.CompactionMode = DevExpress.XtraPrinting.BarCode.QRCodeCompactionMode.Byte; + this.xrBarCode1.Symbology = qrCodeGenerator1; + this.xrBarCode1.Text = "12, rue des Bouchers\r\nMarseille , 13008\r\n91.24.45.40"; + // + // xrLabel4 + // + this.xrLabel4.BackColor = System.Drawing.Color.Transparent; + this.xrLabel4.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel4.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel4.BorderWidth = 1F; + this.xrLabel4.LocationFloat = new DevExpress.Utils.PointFloat(0F, 60.41667F); + this.xrLabel4.Name = "xrLabel4"; + this.xrLabel4.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 0, 100F); + this.xrLabel4.SizeF = new System.Drawing.SizeF(163.3871F, 25F); + this.xrLabel4.StyleName = "Headers"; + this.xrLabel4.StylePriority.UseBackColor = false; + this.xrLabel4.StylePriority.UseBorderColor = false; + this.xrLabel4.StylePriority.UseBorders = false; + this.xrLabel4.StylePriority.UseBorderWidth = false; + this.xrLabel4.StylePriority.UseFont = false; + this.xrLabel4.StylePriority.UseForeColor = false; + this.xrLabel4.StylePriority.UsePadding = false; + this.xrLabel4.StylePriority.UseTextAlignment = false; + this.xrLabel4.Text = "CUSTOMER ACCOUNT:"; + this.xrLabel4.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // xrLabel5 + // + this.xrLabel5.BackColor = System.Drawing.Color.Transparent; + this.xrLabel5.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel5.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel5.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[CustomerAccount]")}); + this.xrLabel5.LocationFloat = new DevExpress.Utils.PointFloat(163.3871F, 60.41667F); + this.xrLabel5.Name = "xrLabel5"; + this.xrLabel5.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 2, 0, 0, 100F); + this.xrLabel5.SizeF = new System.Drawing.SizeF(127.8075F, 25F); + this.xrLabel5.StyleName = "DetailData3"; + this.xrLabel5.StylePriority.UseBackColor = false; + this.xrLabel5.StylePriority.UseBorderColor = false; + this.xrLabel5.StylePriority.UseBorders = false; + this.xrLabel5.StylePriority.UseFont = false; + this.xrLabel5.StylePriority.UseForeColor = false; + this.xrLabel5.StylePriority.UsePadding = false; + this.xrLabel5.StylePriority.UseTextAlignment = false; + this.xrLabel5.Text = "Energy"; + this.xrLabel5.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // xrLabel6 + // + this.xrLabel6.BackColor = System.Drawing.Color.Transparent; + this.xrLabel6.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel6.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel6.BorderWidth = 1F; + this.xrLabel6.LocationFloat = new DevExpress.Utils.PointFloat(0F, 89.58334F); + this.xrLabel6.Name = "xrLabel6"; + this.xrLabel6.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 0, 100F); + this.xrLabel6.SizeF = new System.Drawing.SizeF(163.3871F, 25F); + this.xrLabel6.StyleName = "Headers"; + this.xrLabel6.StylePriority.UseBackColor = false; + this.xrLabel6.StylePriority.UseBorderColor = false; + this.xrLabel6.StylePriority.UseBorders = false; + this.xrLabel6.StylePriority.UseBorderWidth = false; + this.xrLabel6.StylePriority.UseFont = false; + this.xrLabel6.StylePriority.UseForeColor = false; + this.xrLabel6.StylePriority.UsePadding = false; + this.xrLabel6.StylePriority.UseTextAlignment = false; + this.xrLabel6.Text = "CUSTOMER IDENTIFIERS:"; + this.xrLabel6.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // xrLabel7 + // + this.xrLabel7.BackColor = System.Drawing.Color.Transparent; + this.xrLabel7.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel7.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel7.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[CustomerIdentifiers]")}); + this.xrLabel7.LocationFloat = new DevExpress.Utils.PointFloat(163.3871F, 89.58334F); + this.xrLabel7.Name = "xrLabel7"; + this.xrLabel7.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 2, 0, 0, 100F); + this.xrLabel7.SizeF = new System.Drawing.SizeF(127.8075F, 25F); + this.xrLabel7.StyleName = "DetailData3"; + this.xrLabel7.StylePriority.UseBackColor = false; + this.xrLabel7.StylePriority.UseBorderColor = false; + this.xrLabel7.StylePriority.UseBorders = false; + this.xrLabel7.StylePriority.UseFont = false; + this.xrLabel7.StylePriority.UseForeColor = false; + this.xrLabel7.StylePriority.UsePadding = false; + this.xrLabel7.StylePriority.UseTextAlignment = false; + this.xrLabel7.Text = "1273-86"; + this.xrLabel7.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // xrLabel8 + // + this.xrLabel8.BackColor = System.Drawing.Color.Transparent; + this.xrLabel8.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel8.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel8.BorderWidth = 1F; + this.xrLabel8.LocationFloat = new DevExpress.Utils.PointFloat(0F, 118.75F); + this.xrLabel8.Name = "xrLabel8"; + this.xrLabel8.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 0, 100F); + this.xrLabel8.SizeF = new System.Drawing.SizeF(163.3871F, 24.99999F); + this.xrLabel8.StyleName = "Headers"; + this.xrLabel8.StylePriority.UseBackColor = false; + this.xrLabel8.StylePriority.UseBorderColor = false; + this.xrLabel8.StylePriority.UseBorders = false; + this.xrLabel8.StylePriority.UseBorderWidth = false; + this.xrLabel8.StylePriority.UseFont = false; + this.xrLabel8.StylePriority.UseForeColor = false; + this.xrLabel8.StylePriority.UsePadding = false; + this.xrLabel8.StylePriority.UseTextAlignment = false; + this.xrLabel8.Text = "EMAIL:"; + this.xrLabel8.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // xrLabel9 + // + this.xrLabel9.AutoWidth = true; + this.xrLabel9.BackColor = System.Drawing.Color.Transparent; + this.xrLabel9.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel9.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel9.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Email]")}); + this.xrLabel9.LocationFloat = new DevExpress.Utils.PointFloat(163.3871F, 118.75F); + this.xrLabel9.Name = "xrLabel9"; + this.xrLabel9.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 2, 0, 0, 100F); + this.xrLabel9.SizeF = new System.Drawing.SizeF(127.8075F, 24.99997F); + this.xrLabel9.StyleName = "DetailData3"; + this.xrLabel9.StylePriority.UseBackColor = false; + this.xrLabel9.StylePriority.UseBorderColor = false; + this.xrLabel9.StylePriority.UseBorders = false; + this.xrLabel9.StylePriority.UseFont = false; + this.xrLabel9.StylePriority.UseForeColor = false; + this.xrLabel9.StylePriority.UsePadding = false; + this.xrLabel9.StylePriority.UseTextAlignment = false; + this.xrLabel9.Text = "laurence@bon.com"; + this.xrLabel9.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + this.xrLabel9.WordWrap = false; + // + // xrLabel10 + // + this.xrLabel10.BackColor = System.Drawing.Color.Transparent; + this.xrLabel10.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel10.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel10.CanGrow = false; + this.xrLabel10.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Address]")}); + this.xrLabel10.LocationFloat = new DevExpress.Utils.PointFloat(370.0327F, 60.41668F); + this.xrLabel10.Name = "xrLabel10"; + this.xrLabel10.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrLabel10.SizeF = new System.Drawing.SizeF(144.7905F, 25F); + this.xrLabel10.StyleName = "DetailData3"; + this.xrLabel10.StylePriority.UseBackColor = false; + this.xrLabel10.StylePriority.UseBorderColor = false; + this.xrLabel10.StylePriority.UseBorders = false; + this.xrLabel10.StylePriority.UseFont = false; + this.xrLabel10.StylePriority.UseForeColor = false; + this.xrLabel10.StylePriority.UsePadding = false; + this.xrLabel10.StylePriority.UseTextAlignment = false; + this.xrLabel10.Text = "12, rue des Bouchers"; + this.xrLabel10.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + this.xrLabel10.WordWrap = false; + // + // xrLabel11 + // + this.xrLabel11.BackColor = System.Drawing.Color.Transparent; + this.xrLabel11.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel11.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel11.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[City] + \', \' + [Region] + \' \' + [PostalCode]")}); + this.xrLabel11.LocationFloat = new DevExpress.Utils.PointFloat(302.2708F, 89.58334F); + this.xrLabel11.Name = "xrLabel11"; + this.xrLabel11.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrLabel11.SizeF = new System.Drawing.SizeF(212.5539F, 25F); + this.xrLabel11.StyleName = "DetailData3"; + this.xrLabel11.StylePriority.UseBackColor = false; + this.xrLabel11.StylePriority.UseBorderColor = false; + this.xrLabel11.StylePriority.UseBorders = false; + this.xrLabel11.StylePriority.UseFont = false; + this.xrLabel11.StylePriority.UseForeColor = false; + this.xrLabel11.StylePriority.UsePadding = false; + this.xrLabel11.StylePriority.UseTextAlignment = false; + this.xrLabel11.Text = "Marseille, 13008"; + this.xrLabel11.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + // + // xrLabel12 + // + this.xrLabel12.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel12.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel12.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Country]")}); + this.xrLabel12.LocationFloat = new DevExpress.Utils.PointFloat(370.0327F, 118.75F); + this.xrLabel12.Name = "xrLabel12"; + this.xrLabel12.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrLabel12.SizeF = new System.Drawing.SizeF(144.792F, 25.00002F); + this.xrLabel12.StyleName = "DetailData3"; + this.xrLabel12.StylePriority.UseBorderColor = false; + this.xrLabel12.StylePriority.UseBorders = false; + this.xrLabel12.StylePriority.UseFont = false; + this.xrLabel12.StylePriority.UseForeColor = false; + this.xrLabel12.StylePriority.UsePadding = false; + this.xrLabel12.StylePriority.UseTextAlignment = false; + this.xrLabel12.Text = "France"; + this.xrLabel12.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + // + // xrLabel13 + // + this.xrLabel13.BackColor = System.Drawing.Color.Transparent; + this.xrLabel13.BorderColor = System.Drawing.Color.Transparent; + this.xrLabel13.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel13.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Phone]")}); + this.xrLabel13.LocationFloat = new DevExpress.Utils.PointFloat(370.034F, 147.9167F); + this.xrLabel13.Name = "xrLabel13"; + this.xrLabel13.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrLabel13.SizeF = new System.Drawing.SizeF(144.792F, 25F); + this.xrLabel13.StyleName = "DetailData3"; + this.xrLabel13.StylePriority.UseBackColor = false; + this.xrLabel13.StylePriority.UseBorderColor = false; + this.xrLabel13.StylePriority.UseBorders = false; + this.xrLabel13.StylePriority.UseFont = false; + this.xrLabel13.StylePriority.UseForeColor = false; + this.xrLabel13.StylePriority.UsePadding = false; + this.xrLabel13.StylePriority.UseTextAlignment = false; + this.xrLabel13.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + // + // SubBand1 + // + this.SubBand1.Controls.AddRange(new DevExpress.XtraReports.UI.XRControl[] { + this.xrTable5, + this.xrLabel2}); + this.SubBand1.HeightF = 226.7708F; + this.SubBand1.Name = "SubBand1"; + // + // xrTable5 + // + this.xrTable5.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTable5.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrTable5.LocationFloat = new DevExpress.Utils.PointFloat(0F, 48.95833F); + this.xrTable5.Name = "xrTable5"; + this.xrTable5.Rows.AddRange(new DevExpress.XtraReports.UI.XRTableRow[] { + this.xrTableRow9, + this.xrTableRow11, + this.xrTableRow12, + this.xrTableRow10, + this.xrTableRow13}); + this.xrTable5.SizeF = new System.Drawing.SizeF(640.625F, 145.8333F); + this.xrTable5.StylePriority.UseBorderColor = false; + this.xrTable5.StylePriority.UseBorders = false; + // + // xrLabel2 + // + this.xrLabel2.LocationFloat = new DevExpress.Utils.PointFloat(0F, 2.083345F); + this.xrLabel2.Name = "xrLabel2"; + this.xrLabel2.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 2, 0, 0, 100F); + this.xrLabel2.SizeF = new System.Drawing.SizeF(640.625F, 35F); + this.xrLabel2.StyleName = "Title"; + this.xrLabel2.StylePriority.UseBorderColor = false; + this.xrLabel2.StylePriority.UseBorders = false; + this.xrLabel2.StylePriority.UseFont = false; + this.xrLabel2.StylePriority.UseForeColor = false; + this.xrLabel2.StylePriority.UsePadding = false; + this.xrLabel2.StylePriority.UseTextAlignment = false; + this.xrLabel2.Text = "Monthly Billing Invoice Statement"; + this.xrLabel2.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // xrTableRow9 + // + this.xrTableRow9.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell18, + this.xrTableCell20}); + this.xrTableRow9.Name = "xrTableRow9"; + this.xrTableRow9.Weight = 10.208333833436589D; + // + // xrTableRow11 + // + this.xrTableRow11.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell24, + this.xrTableCell25}); + this.xrTableRow11.Name = "xrTableRow11"; + this.xrTableRow11.Weight = 10.208333401633816D; + // + // xrTableRow12 + // + this.xrTableRow12.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell26, + this.xrTableCell27}); + this.xrTableRow12.Name = "xrTableRow12"; + this.xrTableRow12.Weight = 10.208334312922753D; + // + // xrTableRow10 + // + this.xrTableRow10.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell21, + this.xrTableCell23}); + this.xrTableRow10.Name = "xrTableRow10"; + this.xrTableRow10.Weight = 10.208334312922753D; + // + // xrTableRow13 + // + this.xrTableRow13.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell28, + this.xrTableCell29}); + this.xrTableRow13.Name = "xrTableRow13"; + this.xrTableRow13.Weight = 10.208332922147653D; + // + // xrTableCell18 + // + this.xrTableCell18.Font = new DevExpress.Drawing.DXFont("Open Sans", 8.25F, DevExpress.Drawing.DXFontStyle.Bold); + this.xrTableCell18.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.xrTableCell18.Multiline = true; + this.xrTableCell18.Name = "xrTableCell18"; + this.xrTableCell18.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 1, 100F); + this.xrTableCell18.StyleName = "DetailData3_Odd"; + this.xrTableCell18.StylePriority.UseFont = false; + this.xrTableCell18.StylePriority.UseForeColor = false; + this.xrTableCell18.StylePriority.UsePadding = false; + this.xrTableCell18.Text = "CONTACT NAME"; + this.xrTableCell18.Weight = 0.13177048207687855D; + // + // xrTableCell20 + // + this.xrTableCell20.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[ContactName]")}); + this.xrTableCell20.Name = "xrTableCell20"; + this.xrTableCell20.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 2, 0, 1, 100F); + this.xrTableCell20.StyleName = "DetailData3_Odd"; + this.xrTableCell20.StylePriority.UsePadding = false; + this.xrTableCell20.Text = "Laurence Lebihan"; + this.xrTableCell20.Weight = 0.31267396236756584D; + // + // xrTableCell24 + // + this.xrTableCell24.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTableCell24.Name = "xrTableCell24"; + this.xrTableCell24.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 1, 100F); + this.xrTableCell24.StyleName = "Headers"; + this.xrTableCell24.StylePriority.UseBackColor = false; + this.xrTableCell24.StylePriority.UseBorderColor = false; + this.xrTableCell24.StylePriority.UseBorders = false; + this.xrTableCell24.StylePriority.UseFont = false; + this.xrTableCell24.StylePriority.UseForeColor = false; + this.xrTableCell24.StylePriority.UsePadding = false; + this.xrTableCell24.Text = "CONTACT TITLE"; + this.xrTableCell24.Weight = 0.13177048207687855D; + // + // xrTableCell25 + // + this.xrTableCell25.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTableCell25.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[ContactTitle]")}); + this.xrTableCell25.Name = "xrTableCell25"; + this.xrTableCell25.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 2, 0, 1, 100F); + this.xrTableCell25.StyleName = "DetailData3"; + this.xrTableCell25.StylePriority.UseBorderColor = false; + this.xrTableCell25.StylePriority.UseBorders = false; + this.xrTableCell25.StylePriority.UsePadding = false; + this.xrTableCell25.Text = "Owner"; + this.xrTableCell25.Weight = 0.31267396236756584D; + // + // xrTableCell26 + // + this.xrTableCell26.Font = new DevExpress.Drawing.DXFont("Open Sans", 8.25F, DevExpress.Drawing.DXFontStyle.Bold); + this.xrTableCell26.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.xrTableCell26.Name = "xrTableCell26"; + this.xrTableCell26.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 1, 100F); + this.xrTableCell26.StyleName = "DetailData3_Odd"; + this.xrTableCell26.StylePriority.UseFont = false; + this.xrTableCell26.StylePriority.UseForeColor = false; + this.xrTableCell26.StylePriority.UsePadding = false; + this.xrTableCell26.Text = "BILLING STATEMENT DATE"; + this.xrTableCell26.Weight = 0.13177048207687855D; + // + // xrTableCell27 + // + this.xrTableCell27.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[BillingDate]")}); + this.xrTableCell27.Name = "xrTableCell27"; + this.xrTableCell27.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 2, 0, 1, 100F); + this.xrTableCell27.StyleName = "DetailData3_Odd"; + this.xrTableCell27.StylePriority.UsePadding = false; + this.xrTableCell27.Text = "9/5/2018"; + this.xrTableCell27.TextFormatString = "{0:M/d/yyyy}"; + this.xrTableCell27.Weight = 0.31267396236756584D; + // + // xrTableCell21 + // + this.xrTableCell21.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTableCell21.Name = "xrTableCell21"; + this.xrTableCell21.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 1, 100F); + this.xrTableCell21.StyleName = "Headers"; + this.xrTableCell21.StylePriority.UseBackColor = false; + this.xrTableCell21.StylePriority.UseBorderColor = false; + this.xrTableCell21.StylePriority.UseBorders = false; + this.xrTableCell21.StylePriority.UseFont = false; + this.xrTableCell21.StylePriority.UseForeColor = false; + this.xrTableCell21.StylePriority.UsePadding = false; + this.xrTableCell21.Text = "BILLING PERIOD"; + this.xrTableCell21.Weight = 0.13177048207687855D; + // + // xrTableCell23 + // + this.xrTableCell23.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTableCell23.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "FormatString(\'{0:M/d/yyyy}\',[BillingPeriodStart] ) + \' To \' + FormatString(\'{0:M/" + + "d/yyyy}\',[BillingPeriodEnd])")}); + this.xrTableCell23.Name = "xrTableCell23"; + this.xrTableCell23.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 2, 0, 1, 100F); + this.xrTableCell23.StyleName = "DetailData3"; + this.xrTableCell23.StylePriority.UseBorderColor = false; + this.xrTableCell23.StylePriority.UseBorders = false; + this.xrTableCell23.StylePriority.UsePadding = false; + this.xrTableCell23.Text = "8/4/2018 To 8/17/2018"; + this.xrTableCell23.Weight = 0.31267396236756584D; + // + // xrTableCell28 + // + this.xrTableCell28.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTableCell28.Font = new DevExpress.Drawing.DXFont("Open Sans", 8.25F, DevExpress.Drawing.DXFontStyle.Bold); + this.xrTableCell28.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.xrTableCell28.Name = "xrTableCell28"; + this.xrTableCell28.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 1, 100F); + this.xrTableCell28.StyleName = "DetailData3_Odd"; + this.xrTableCell28.StylePriority.UseBorderColor = false; + this.xrTableCell28.StylePriority.UseBorders = false; + this.xrTableCell28.StylePriority.UseFont = false; + this.xrTableCell28.StylePriority.UseForeColor = false; + this.xrTableCell28.StylePriority.UsePadding = false; + this.xrTableCell28.Text = "TERMS"; + this.xrTableCell28.Weight = 0.13177048207687855D; + // + // xrTableCell29 + // + this.xrTableCell29.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTableCell29.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Terms]")}); + this.xrTableCell29.Name = "xrTableCell29"; + this.xrTableCell29.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 2, 0, 1, 100F); + this.xrTableCell29.StyleName = "DetailData3_Odd"; + this.xrTableCell29.StylePriority.UseBorderColor = false; + this.xrTableCell29.StylePriority.UseBorders = false; + this.xrTableCell29.StylePriority.UsePadding = false; + this.xrTableCell29.Text = "End of month"; + this.xrTableCell29.Weight = 0.31267396236756584D; + // + // groupHeaderBand1 + // + this.groupHeaderBand1.Controls.AddRange(new DevExpress.XtraReports.UI.XRControl[] { + this.xrTable2}); + this.groupHeaderBand1.GroupUnion = DevExpress.XtraReports.UI.GroupUnion.WithFirstDetail; + this.groupHeaderBand1.HeightF = 29.16667F; + this.groupHeaderBand1.Name = "groupHeaderBand1"; + // + // detailBand2 + // + this.detailBand2.Controls.AddRange(new DevExpress.XtraReports.UI.XRControl[] { + this.xrTable3}); + this.detailBand2.EvenStyleName = "DetailData3"; + this.detailBand2.HeightF = 29.16667F; + this.detailBand2.Name = "detailBand2"; + // + // ReportHeader + // + this.ReportHeader.Controls.AddRange(new DevExpress.XtraReports.UI.XRControl[] { + this.xrLabel3}); + this.ReportHeader.HeightF = 46.875F; + this.ReportHeader.Name = "ReportHeader"; + // + // ReportFooter + // + this.ReportFooter.Controls.AddRange(new DevExpress.XtraReports.UI.XRControl[] { + this.xrTable1}); + this.ReportFooter.HeightF = 98.95834F; + this.ReportFooter.Name = "ReportFooter"; + this.ReportFooter.PrintAtBottom = true; + // + // xrTable2 + // + this.xrTable2.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTable2.Borders = ((DevExpress.XtraPrinting.BorderSide)((((DevExpress.XtraPrinting.BorderSide.Left | DevExpress.XtraPrinting.BorderSide.Top) + | DevExpress.XtraPrinting.BorderSide.Right) + | DevExpress.XtraPrinting.BorderSide.Bottom))); + this.xrTable2.BorderWidth = 1.2F; + this.xrTable2.LocationFloat = new DevExpress.Utils.PointFloat(4.238557E-05F, 0F); + this.xrTable2.Name = "xrTable2"; + this.xrTable2.Rows.AddRange(new DevExpress.XtraReports.UI.XRTableRow[] { + this.xrTableRow3}); + this.xrTable2.SizeF = new System.Drawing.SizeF(640.625F, 29.16667F); + this.xrTable2.StyleName = "Headers"; + this.xrTable2.StylePriority.UseBorderWidth = false; + // + // xrTableRow3 + // + this.xrTableRow3.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell7, + this.xrTableCell8, + this.xrTableCell9, + this.xrTableCell11}); + this.xrTableRow3.Name = "xrTableRow3"; + this.xrTableRow3.StyleName = "Headers"; + this.xrTableRow3.Weight = 1.0416666991890433D; + // + // xrTableCell7 + // + this.xrTableCell7.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.xrTableCell7.Borders = DevExpress.XtraPrinting.BorderSide.Bottom; + this.xrTableCell7.BorderWidth = 2F; + this.xrTableCell7.Name = "xrTableCell7"; + this.xrTableCell7.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 0, 100F); + this.xrTableCell7.StyleName = "Headers"; + this.xrTableCell7.StylePriority.UseBorderColor = false; + this.xrTableCell7.StylePriority.UseBorders = false; + this.xrTableCell7.StylePriority.UseBorderWidth = false; + this.xrTableCell7.StylePriority.UsePadding = false; + this.xrTableCell7.Text = "DATE"; + this.xrTableCell7.Weight = 0.26282052324906857D; + // + // xrTableCell8 + // + this.xrTableCell8.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.xrTableCell8.Borders = DevExpress.XtraPrinting.BorderSide.Bottom; + this.xrTableCell8.BorderWidth = 2F; + this.xrTableCell8.Name = "xrTableCell8"; + this.xrTableCell8.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrTableCell8.StyleName = "Headers"; + this.xrTableCell8.StylePriority.UseBorderColor = false; + this.xrTableCell8.StylePriority.UseBorders = false; + this.xrTableCell8.StylePriority.UseBorderWidth = false; + this.xrTableCell8.Text = "DESCRIPTION"; + this.xrTableCell8.Weight = 0.2264102459817007D; + // + // xrTableCell9 + // + this.xrTableCell9.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.xrTableCell9.Borders = DevExpress.XtraPrinting.BorderSide.Bottom; + this.xrTableCell9.BorderWidth = 2F; + this.xrTableCell9.Name = "xrTableCell9"; + this.xrTableCell9.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrTableCell9.StyleName = "Headers"; + this.xrTableCell9.StylePriority.UseBorderColor = false; + this.xrTableCell9.StylePriority.UseBorders = false; + this.xrTableCell9.StylePriority.UseBorderWidth = false; + this.xrTableCell9.StylePriority.UseTextAlignment = false; + this.xrTableCell9.Text = "AMOUNT"; + this.xrTableCell9.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + this.xrTableCell9.Weight = 0.24679486710782655D; + // + // xrTableCell11 + // + this.xrTableCell11.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.xrTableCell11.Borders = DevExpress.XtraPrinting.BorderSide.Bottom; + this.xrTableCell11.BorderWidth = 2F; + this.xrTableCell11.Name = "xrTableCell11"; + this.xrTableCell11.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 10, 0, 0, 100F); + this.xrTableCell11.StyleName = "Headers"; + this.xrTableCell11.StylePriority.UseBorderColor = false; + this.xrTableCell11.StylePriority.UseBorders = false; + this.xrTableCell11.StylePriority.UseBorderWidth = false; + this.xrTableCell11.StylePriority.UsePadding = false; + this.xrTableCell11.StylePriority.UseTextAlignment = false; + this.xrTableCell11.Text = "BALANCE"; + this.xrTableCell11.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + this.xrTableCell11.Weight = 0.26397427885888147D; + // + // xrTable3 + // + this.xrTable3.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrTable3.BorderWidth = 0.8F; + this.xrTable3.EvenStyleName = "DetailData3_Odd"; + this.xrTable3.LocationFloat = new DevExpress.Utils.PointFloat(5.298196E-05F, 0F); + this.xrTable3.Name = "xrTable3"; + this.xrTable3.OddStyleName = "DetailData3"; + this.xrTable3.Rows.AddRange(new DevExpress.XtraReports.UI.XRTableRow[] { + this.xrTableRow4}); + this.xrTable3.SizeF = new System.Drawing.SizeF(640.6249F, 29.16667F); + this.xrTable3.StylePriority.UseBorderColor = false; + this.xrTable3.StylePriority.UseBorders = false; + this.xrTable3.StylePriority.UseBorderWidth = false; + // + // xrTableRow4 + // + this.xrTableRow4.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell12, + this.xrTableCell13, + this.xrTableCell15, + this.xrTableCell16}); + this.xrTableRow4.Name = "xrTableRow4"; + this.xrTableRow4.Weight = 13.416666666666666D; + // + // xrTableCell12 + // + this.xrTableCell12.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Date]")}); + this.xrTableCell12.Name = "xrTableCell12"; + this.xrTableCell12.Padding = new DevExpress.XtraPrinting.PaddingInfo(10, 2, 0, 0, 100F); + this.xrTableCell12.StyleName = "DetailData3"; + this.xrTableCell12.StylePriority.UsePadding = false; + this.xrTableCell12.Text = "9/9/2018"; + this.xrTableCell12.TextFormatString = "{0:M/d/yyyy}"; + this.xrTableCell12.Weight = 0.22071864610205361D; + // + // xrTableCell13 + // + this.xrTableCell13.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[Description]")}); + this.xrTableCell13.Name = "xrTableCell13"; + this.xrTableCell13.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrTableCell13.StyleName = "DetailData3"; + this.xrTableCell13.StylePriority.UsePadding = false; + this.xrTableCell13.Text = "Bill - Rent"; + this.xrTableCell13.Weight = 0.190140978530862D; + // + // xrTableCell15 + // + this.xrTableCell15.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "Iif(Contains([Description], \'Balance\'), \'\', [Amount])")}); + this.xrTableCell15.Name = "xrTableCell15"; + this.xrTableCell15.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 2, 0, 0, 100F); + this.xrTableCell15.StyleName = "DetailData3"; + this.xrTableCell15.StylePriority.UsePadding = false; + this.xrTableCell15.StylePriority.UseTextAlignment = false; + this.xrTableCell15.Text = "$210.00"; + this.xrTableCell15.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + this.xrTableCell15.TextFormatString = "{0:$#,##.00}"; + this.xrTableCell15.Weight = 0.20726019714891966D; + // + // xrTableCell16 + // + this.xrTableCell16.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "sumRunningSum([Amount])"), + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Font.Name", "\'Open Sans\'"), + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Font.Size", "8"), + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Font.Bold", "[DataSource.CurrentRowIndex] == [DataSource.RowCount] -1")}); + this.xrTableCell16.Font = new DevExpress.Drawing.DXFont("Open Sans", 8F, DevExpress.Drawing.DXFontStyle.Bold); + this.xrTableCell16.Name = "xrTableCell16"; + this.xrTableCell16.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 10, 0, 0, 100F); + this.xrTableCell16.StyleName = "DetailData3"; + this.xrTableCell16.StylePriority.UseFont = false; + this.xrTableCell16.StylePriority.UsePadding = false; + this.xrTableCell16.StylePriority.UseTextAlignment = false; + xrSummary1.Running = DevExpress.XtraReports.UI.SummaryRunning.Report; + this.xrTableCell16.Summary = xrSummary1; + this.xrTableCell16.Text = "xrTableCell16"; + this.xrTableCell16.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleRight; + this.xrTableCell16.TextFormatString = "{0:$#,##.00}"; + this.xrTableCell16.Weight = 0.22168754127396906D; + // + // xrLabel3 + // + this.xrLabel3.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(75)))), ((int)(((byte)(75)))), ((int)(((byte)(75))))); + this.xrLabel3.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.xrLabel3.LocationFloat = new DevExpress.Utils.PointFloat(0F, 2.083306F); + this.xrLabel3.Name = "xrLabel3"; + this.xrLabel3.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 2, 0, 0, 100F); + this.xrLabel3.SizeF = new System.Drawing.SizeF(640.625F, 27.94597F); + this.xrLabel3.StyleName = "Title"; + this.xrLabel3.StylePriority.UseBorderColor = false; + this.xrLabel3.StylePriority.UseBorders = false; + this.xrLabel3.StylePriority.UseFont = false; + this.xrLabel3.StylePriority.UseForeColor = false; + this.xrLabel3.StylePriority.UsePadding = false; + this.xrLabel3.StylePriority.UseTextAlignment = false; + this.xrLabel3.Text = "Payments and Adjustments"; + this.xrLabel3.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // xrTable1 + // + this.xrTable1.LocationFloat = new DevExpress.Utils.PointFloat(0F, 0F); + this.xrTable1.Name = "xrTable1"; + this.xrTable1.Rows.AddRange(new DevExpress.XtraReports.UI.XRTableRow[] { + this.xrTableRow1, + this.xrTableRow2}); + this.xrTable1.SizeF = new System.Drawing.SizeF(650F, 98.95834F); + // + // xrTableRow1 + // + this.xrTableRow1.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell3, + this.xrTableCell1, + this.xrTableCell2}); + this.xrTableRow1.KeepTogether = false; + this.xrTableRow1.Name = "xrTableRow1"; + this.xrTableRow1.Weight = 0.299592056274414D; + // + // xrTableRow2 + // + this.xrTableRow2.Cells.AddRange(new DevExpress.XtraReports.UI.XRTableCell[] { + this.xrTableCell6, + this.xrTableCell4, + this.xrTableCell5}); + this.xrTableRow2.KeepTogether = false; + this.xrTableRow2.Name = "xrTableRow2"; + this.xrTableRow2.Weight = 0.70040771484374975D; + // + // xrTableCell3 + // + this.xrTableCell3.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(238)))), ((int)(((byte)(241)))), ((int)(((byte)(246))))); + this.xrTableCell3.Borders = DevExpress.XtraPrinting.BorderSide.Right; + this.xrTableCell3.BorderWidth = 2F; + this.xrTableCell3.Multiline = true; + this.xrTableCell3.Name = "xrTableCell3"; + this.xrTableCell3.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrTableCell3.StyleName = "SummaryTitles"; + this.xrTableCell3.StylePriority.UseBorderColor = false; + this.xrTableCell3.StylePriority.UseBorders = false; + this.xrTableCell3.StylePriority.UseBorderWidth = false; + this.xrTableCell3.StylePriority.UseTextAlignment = false; + this.xrTableCell3.Text = "NEW CHARGES\r\n"; + this.xrTableCell3.TextAlignment = DevExpress.XtraPrinting.TextAlignment.BottomCenter; + this.xrTableCell3.Weight = 0.9903846153846152D; + // + // xrTableCell1 + // + this.xrTableCell1.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(238)))), ((int)(((byte)(241)))), ((int)(((byte)(246))))); + this.xrTableCell1.Borders = DevExpress.XtraPrinting.BorderSide.Right; + this.xrTableCell1.BorderWidth = 2F; + this.xrTableCell1.Multiline = true; + this.xrTableCell1.Name = "xrTableCell1"; + this.xrTableCell1.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrTableCell1.StyleName = "SummaryTitles"; + this.xrTableCell1.StylePriority.UseBorderColor = false; + this.xrTableCell1.StylePriority.UseBorders = false; + this.xrTableCell1.StylePriority.UseBorderWidth = false; + this.xrTableCell1.StylePriority.UseTextAlignment = false; + this.xrTableCell1.Text = "PAYMENTS"; + this.xrTableCell1.TextAlignment = DevExpress.XtraPrinting.TextAlignment.BottomCenter; + this.xrTableCell1.Weight = 1D; + // + // xrTableCell2 + // + this.xrTableCell2.Multiline = true; + this.xrTableCell2.Name = "xrTableCell2"; + this.xrTableCell2.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + this.xrTableCell2.StyleName = "SummaryTitles"; + this.xrTableCell2.StylePriority.UseBorderColor = false; + this.xrTableCell2.StylePriority.UseBorders = false; + this.xrTableCell2.StylePriority.UseBorderWidth = false; + this.xrTableCell2.StylePriority.UsePadding = false; + this.xrTableCell2.StylePriority.UseTextAlignment = false; + this.xrTableCell2.Text = "CURRENT BALANCE"; + this.xrTableCell2.TextAlignment = DevExpress.XtraPrinting.TextAlignment.BottomCenter; + this.xrTableCell2.Weight = 1.0096153846153846D; + // + // xrTableCell6 + // + this.xrTableCell6.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(238)))), ((int)(((byte)(241)))), ((int)(((byte)(246))))); + this.xrTableCell6.Borders = DevExpress.XtraPrinting.BorderSide.Right; + this.xrTableCell6.BorderWidth = 2F; + this.xrTableCell6.CanGrow = false; + this.xrTableCell6.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[][[Amount] > 0 And !Contains([Description], \'Balance\')].Sum([Amount])")}); + this.xrTableCell6.Font = new DevExpress.Drawing.DXFont("Open Sans", 23F, DevExpress.Drawing.DXFontStyle.Bold); + this.xrTableCell6.Multiline = true; + this.xrTableCell6.Name = "xrTableCell6"; + this.xrTableCell6.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 0, 2, 0, 100F); + this.xrTableCell6.StyleName = "SummaryValues"; + this.xrTableCell6.StylePriority.UseBorderColor = false; + this.xrTableCell6.StylePriority.UseBorders = false; + this.xrTableCell6.StylePriority.UseBorderWidth = false; + this.xrTableCell6.StylePriority.UseFont = false; + this.xrTableCell6.StylePriority.UsePadding = false; + this.xrTableCell6.Text = "+$210.00"; + this.xrTableCell6.TextFormatString = "+{0:$#,##.00}"; + this.xrTableCell6.Weight = 0.9903846153846152D; + // + // xrTableCell4 + // + this.xrTableCell4.BorderColor = System.Drawing.Color.FromArgb(((int)(((byte)(238)))), ((int)(((byte)(241)))), ((int)(((byte)(246))))); + this.xrTableCell4.Borders = DevExpress.XtraPrinting.BorderSide.Right; + this.xrTableCell4.BorderWidth = 2F; + this.xrTableCell4.CanGrow = false; + this.xrTableCell4.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "[][[Amount] < 0].Sum([Amount])")}); + this.xrTableCell4.Font = new DevExpress.Drawing.DXFont("Open Sans", 23F, DevExpress.Drawing.DXFontStyle.Bold); + this.xrTableCell4.Multiline = true; + this.xrTableCell4.Name = "xrTableCell4"; + this.xrTableCell4.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 0, 2, 0, 100F); + this.xrTableCell4.StyleName = "SummaryValues"; + this.xrTableCell4.StylePriority.UseBorderColor = false; + this.xrTableCell4.StylePriority.UseBorders = false; + this.xrTableCell4.StylePriority.UseBorderWidth = false; + this.xrTableCell4.StylePriority.UseFont = false; + this.xrTableCell4.StylePriority.UsePadding = false; + this.xrTableCell4.Text = "-$280.00"; + this.xrTableCell4.TextFormatString = "{0:$#,##.00}"; + this.xrTableCell4.Weight = 1D; + // + // xrTableCell5 + // + this.xrTableCell5.CanGrow = false; + this.xrTableCell5.ExpressionBindings.AddRange(new DevExpress.XtraReports.UI.ExpressionBinding[] { + new DevExpress.XtraReports.UI.ExpressionBinding("BeforePrint", "Text", "Sum([Amount])")}); + this.xrTableCell5.Font = new DevExpress.Drawing.DXFont("Open Sans", 23F, DevExpress.Drawing.DXFontStyle.Bold); + this.xrTableCell5.Multiline = true; + this.xrTableCell5.Name = "xrTableCell5"; + this.xrTableCell5.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 0, 2, 0, 100F); + this.xrTableCell5.StyleName = "SummaryValues"; + this.xrTableCell5.StylePriority.UseBorderColor = false; + this.xrTableCell5.StylePriority.UseBorders = false; + this.xrTableCell5.StylePriority.UseBorderWidth = false; + this.xrTableCell5.StylePriority.UseFont = false; + this.xrTableCell5.StylePriority.UsePadding = false; + this.xrTableCell5.Text = "$1,480.00"; + this.xrTableCell5.TextFormatString = "{0:$#,##.00}"; + this.xrTableCell5.Weight = 1.0096153846153846D; + // + // objectDataSource1 + // + parameter1.Name = "rowCount"; + parameter1.Type = typeof(DevExpress.DataAccess.Expression); + parameter1.Value = new DevExpress.DataAccess.Expression("[Parameters.RowCountParameter]", typeof(int)); + objectConstructorInfo1.Parameters.AddRange(new DevExpress.DataAccess.ObjectBinding.Parameter[] { + parameter1}); + this.objectDataSource1.Constructor = objectConstructorInfo1; + this.objectDataSource1.DataSource = typeof(EnvelopeGenerator.Server.Client.Data.DataItemList); + this.objectDataSource1.Name = "objectDataSource1"; + // + // Title + // + this.Title.BackColor = System.Drawing.Color.Transparent; + this.Title.BorderColor = System.Drawing.Color.Black; + this.Title.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.Title.BorderWidth = 1F; + this.Title.Font = new DevExpress.Drawing.DXFont("Open Sans", 13F, DevExpress.Drawing.DXFontStyle.Bold); + this.Title.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(73)))), ((int)(((byte)(80)))), ((int)(((byte)(87))))); + this.Title.Name = "Title"; + // + // ReportTitleCaption + // + this.ReportTitleCaption.BackColor = System.Drawing.Color.Transparent; + this.ReportTitleCaption.BorderColor = System.Drawing.Color.Transparent; + this.ReportTitleCaption.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.ReportTitleCaption.Font = new DevExpress.Drawing.DXFont("Open Sans", 20F, DevExpress.Drawing.DXFontStyle.Bold); + this.ReportTitleCaption.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(73)))), ((int)(((byte)(80)))), ((int)(((byte)(87))))); + this.ReportTitleCaption.Name = "ReportTitleCaption"; + this.ReportTitleCaption.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 6, 0, 0, 100F); + this.ReportTitleCaption.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // DetailData3 + // + this.DetailData3.Font = new DevExpress.Drawing.DXFont("Open Sans", 8.25F); + this.DetailData3.ForeColor = System.Drawing.Color.Black; + this.DetailData3.Name = "DetailData3"; + this.DetailData3.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 6, 0, 0, 100F); + this.DetailData3.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // DetailData3_Odd + // + this.DetailData3_Odd.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(249)))), ((int)(((byte)(250)))), ((int)(((byte)(252))))); + this.DetailData3_Odd.BorderColor = System.Drawing.Color.Transparent; + this.DetailData3_Odd.Borders = DevExpress.XtraPrinting.BorderSide.None; + this.DetailData3_Odd.BorderWidth = 1F; + this.DetailData3_Odd.Font = new DevExpress.Drawing.DXFont("Open Sans", 8.25F); + this.DetailData3_Odd.ForeColor = System.Drawing.Color.Black; + this.DetailData3_Odd.Name = "DetailData3_Odd"; + this.DetailData3_Odd.Padding = new DevExpress.XtraPrinting.PaddingInfo(6, 6, 0, 0, 100F); + this.DetailData3_Odd.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // PageInfo + // + this.PageInfo.Font = new DevExpress.Drawing.DXFont("Open Sans", 8.25F, DevExpress.Drawing.DXFontStyle.Bold); + this.PageInfo.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(73)))), ((int)(((byte)(80)))), ((int)(((byte)(87))))); + this.PageInfo.Name = "PageInfo"; + this.PageInfo.Padding = new DevExpress.XtraPrinting.PaddingInfo(2, 2, 0, 0, 100F); + // + // Headers + // + this.Headers.Font = new DevExpress.Drawing.DXFont("Open Sans", 8.25F, DevExpress.Drawing.DXFontStyle.Bold); + this.Headers.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.Headers.Name = "Headers"; + this.Headers.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleLeft; + // + // SummaryTitles + // + this.SummaryTitles.Font = new DevExpress.Drawing.DXFont("Open Sans", 9F, DevExpress.Drawing.DXFontStyle.Bold); + this.SummaryTitles.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(171)))), ((int)(((byte)(185)))), ((int)(((byte)(214))))); + this.SummaryTitles.Name = "SummaryTitles"; + this.SummaryTitles.TextAlignment = DevExpress.XtraPrinting.TextAlignment.MiddleCenter; + // + // SummaryValues + // + this.SummaryValues.Font = new DevExpress.Drawing.DXFont("Open Sans", 23F); + this.SummaryValues.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(46)))), ((int)(((byte)(94)))), ((int)(((byte)(168))))); + this.SummaryValues.Name = "SummaryValues"; + this.SummaryValues.Padding = new DevExpress.XtraPrinting.PaddingInfo(0, 0, 16, 0, 100F); + this.SummaryValues.TextAlignment = DevExpress.XtraPrinting.TextAlignment.TopCenter; + // + // RowCountParameter + // + this.RowCountParameter.Description = "Row Count"; + this.RowCountParameter.Name = "RowCountParameter"; + this.RowCountParameter.Type = typeof(int); + this.RowCountParameter.ValueInfo = "100000"; + // + // Report + // + this.Bands.AddRange(new DevExpress.XtraReports.UI.Band[] { + this.topMarginBand1, + this.bottomMarginBand1, + this.detailBand1, + this.detailReportBand1}); + this.ComponentStorage.AddRange(new System.ComponentModel.IComponent[] { + this.objectDataSource1}); + this.DataSource = this.objectDataSource1; + this.DisplayName = "Large Dataset"; + this.Font = new DevExpress.Drawing.DXFont("Open Sans", 9.75F); + this.Parameters.AddRange(new DevExpress.XtraReports.Parameters.Parameter[] { + this.RowCountParameter}); + this.RequestParameters = false; + this.StyleSheet.AddRange(new DevExpress.XtraReports.UI.XRControlStyle[] { + this.Title, + this.ReportTitleCaption, + this.DetailData3, + this.DetailData3_Odd, + this.PageInfo, + this.Headers, + this.SummaryTitles, + this.SummaryValues}); + this.Version = "23.1"; + ((System.ComponentModel.ISupportInitialize)(this.xrTable5)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.xrTable2)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.xrTable3)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.xrTable1)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.objectDataSource1)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this)).EndInit(); + + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/Report.resx b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/Report.resx new file mode 100644 index 00000000..ed290a7e --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/Report.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/ReportsFactory.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/ReportsFactory.cs new file mode 100644 index 00000000..8188a439 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/PredefinedReports/ReportsFactory.cs @@ -0,0 +1,14 @@ +using DevExpress.XtraReports.UI; + +namespace EnvelopeGenerator.Server.Client.PredefinedReports { + public static class ReportsFactory + { + public static readonly Dictionary> Reports = new() { + ["LargeDatasetReport"] = () => new PredefinedReports.Report() + }; + + public static XtraReport GetReport(string reportName) { + return Reports[reportName](); + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs new file mode 100644 index 00000000..d21cf620 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Program.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using EnvelopeGenerator.Server.Client.Services; +using EnvelopeGenerator.Server.Client.Options; +using DevExpress.Blazor.Reporting; +using DevExpress.XtraReports.Web.Extensions; +using DevExpress.DataAccess.Web; +using DevExpress.XtraReports.Services; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +// Named HttpClient for API calls (both for services and DevExpress components) +builder.Services.AddHttpClient("EnvelopeGenerator.Server", client => +{ + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); +}); + +// Default HttpClient (DevExpress PdfViewer requires this) +builder.Services.AddScoped(sp => new HttpClient +{ + BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) +}); + +// Configuration Options +builder.Services.Configure(opts => + builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts)); + +// Business Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +// DevExpress WASM +builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer(); +builder.Services.AddDevExpressWebAssemblyBlazorReportViewer(); + +builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => { + configure.UseDevelopmentMode(); +}); + +// Reporting Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.Data.DataItemList)); +DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.PredefinedReports.Report)); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => sp.GetRequiredService()); +builder.Services.AddScoped(); + +ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension()); + +var host = builder.Build(); +await FontLoader.LoadFonts(host.Services.GetRequiredService(), new List { "opensans.ttf" }); +await host.RunAsync(); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Routes.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Routes.razor new file mode 100644 index 00000000..f756e19d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AnnotationService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AnnotationService.cs new file mode 100644 index 00000000..fd9e7556 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AnnotationService.cs @@ -0,0 +1,29 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using EnvelopeGenerator.Server.Client.Models; + +namespace EnvelopeGenerator.Server.Client.Services; + +/// +/// Retrieves annotation positions from the API. +/// Uses relative paths (/api/Annotation/{envelopeKey}). +/// +[Obsolete("Use SignatureService.")] +public class AnnotationService(IHttpClientFactory httpClientFactory) +{ + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + + public async Task> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var url = $"/api/Annotation/{Uri.EscapeDataString(envelopeKey)}"; + var response = await http.GetAsync(url, cancel); + + if (!response.IsSuccessStatusCode) + return []; + + var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel); + return result ?? []; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AppVersionService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AppVersionService.cs new file mode 100644 index 00000000..b65f37a4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AppVersionService.cs @@ -0,0 +1,26 @@ +namespace EnvelopeGenerator.Server.Client.Services; + +/// +/// Provides application version for cache busting static assets. +/// Version is automatically incremented on each build via AssemblyVersion. +/// +public class AppVersionService +{ + /// + /// Current application version (e.g., "1.0.0.0") + /// + public string Version { get; } + + public AppVersionService() + { + // Get version from assembly metadata + Version = typeof(AppVersionService).Assembly.GetName().Version?.ToString() ?? "1.0.0.0"; + } + + /// + /// Generates versioned URL for static assets (cache busting) + /// + /// Asset path (e.g., "css/envelope-viewer.css") + /// Versioned URL (e.g., "css/envelope-viewer.css?v=1.0.0.0") + public string GetVersionedUrl(string path) => $"{path}?v={Version}"; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs new file mode 100644 index 00000000..bb8658a6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs @@ -0,0 +1,109 @@ +using System.Net; +using System.Net.Http.Json; + +namespace EnvelopeGenerator.Server.Client.Services; + +public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error } + +public enum SenderLoginResult { Success, InvalidCredentials, Error } + +public class AuthService(IHttpClientFactory httpClientFactory) +{ + private HttpClient CreateDefaultClient() => httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + + /// + /// Checks whether the current user holds a valid receiver token for the given envelope key. + /// Calls GET /api/auth/check/envelope/{envelopeKey}. + /// + public async Task CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var response = await http.GetAsync($"/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel); + return response.StatusCode == HttpStatusCode.OK; + } + + /// + /// Checks whether the current user holds a valid receiver token for the given envelope key. + /// Calls GET /api/auth/check/envelope/{envelopeKey}. + /// + public async Task CheckSenderAccessAsync(CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var response = await http.GetAsync($"/api/auth/check", cancel); + return response.StatusCode == HttpStatusCode.OK; + } + + /// + /// Submits the access code for the given envelope key. + /// Calls POST /api/Auth/envelope-receiver/{key} with multipart/form-data. + /// On success the API sets an authentication cookie automatically. + /// + public async Task LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var form = new MultipartFormDataContent + { + { new StringContent(accessCode), "AccessCode" } + }; + + var response = await http.PostAsync( + $"/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}", + form, cancel); + + return response.StatusCode switch + { + HttpStatusCode.OK => EnvelopeLoginResult.Success, + HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode, + HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound, + _ => EnvelopeLoginResult.Error + }; + } + + /// + /// Removes the per-envelope receiver cookie for the given envelope key. + /// Calls POST /api/auth/logout/envelope/{envelopeKey}. + /// + public async Task LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var response = await http.PostAsync( + $"/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}", + null, cancel); + return response.IsSuccessStatusCode; + } + + /// + /// Removes the per-envelope receiver cookie for the given envelope key. + /// Calls POST /api/auth/logout/envelope/{envelopeKey}. + /// + public async Task LogoutSenderAsync(CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var response = await http.PostAsync( + $"/api/auth/logout", + null, cancel); + return response.IsSuccessStatusCode; + } + + /// + /// Authenticates a sender user with username and password. + /// Calls POST /api/auth?cookie=true with JSON body. + /// On success the API sets an authentication cookie automatically. + /// + public async Task LoginSenderAsync(string username, string password, CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var requestBody = new { username, password }; + + var response = await http.PostAsJsonAsync( + $"/api/auth?cookie=true", + requestBody, cancel); + + return response.StatusCode switch + { + HttpStatusCode.OK => SenderLoginResult.Success, + HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials, + _ => SenderLoginResult.Error + }; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CultureService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CultureService.cs new file mode 100644 index 00000000..a7b6a7b6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CultureService.cs @@ -0,0 +1,74 @@ +using System.Globalization; +using Microsoft.JSInterop; + +namespace EnvelopeGenerator.Server.Client.Services; + +/// +/// Service for managing application culture/localization. +/// +public class CultureService +{ + private readonly IJSRuntime _jsRuntime; + private const string CULTURE_KEY = "AppCulture"; + + public CultureService(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + /// + /// Gets the list of supported cultures. + /// + public static CultureInfo[] SupportedCultures { get; } = new[] + { + new CultureInfo("de-DE"), + new CultureInfo("en-US"), + new CultureInfo("fr-FR") + }; + + /// + /// Sets the application culture and stores it in localStorage. + /// + public async Task SetCultureAsync(string culture) + { + if (!SupportedCultures.Any(c => c.Name == culture)) + throw new ArgumentException($"Culture '{culture}' is not supported.", nameof(culture)); + + await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CULTURE_KEY, culture); + } + + /// + /// Gets the stored culture from localStorage. + /// + public async Task GetCultureAsync() + { + try + { + return await _jsRuntime.InvokeAsync("localStorage.getItem", CULTURE_KEY); + } + catch + { + return null; + } + } + + /// + /// Initializes the culture from localStorage or browser settings. + /// + public async Task InitializeCultureAsync() + { + var storedCulture = await GetCultureAsync(); + + if (!string.IsNullOrEmpty(storedCulture) && + SupportedCultures.Any(c => c.Name == storedCulture)) + { + return new CultureInfo(storedCulture); + } + + // Fallback to browser culture or default + var browserCulture = CultureInfo.CurrentCulture.Name; + var matchedCulture = SupportedCultures.FirstOrDefault(c => c.Name == browserCulture); + + return matchedCulture ?? SupportedCultures[0]; // Default to German + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomDataSourceWizardJsonDataConnectionStorage.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomDataSourceWizardJsonDataConnectionStorage.cs new file mode 100644 index 00000000..306bf72a --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomDataSourceWizardJsonDataConnectionStorage.cs @@ -0,0 +1,39 @@ +using DevExpress.DataAccess.Json; +using DevExpress.DataAccess.Web; +using DevExpress.DataAccess.Wizard.Services; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage +{ + public static JsonDataConnection GetDefaultConnection() { + var uriJsonSource = new UriJsonSource() { + Uri = new Uri(@"https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"), + }; + return new JsonDataConnection(uriJsonSource) { StoreConnectionNameOnly = true, Name = "NWindProductsJson" }; + } + public static List GetConnections() { + var connections = new List { + GetDefaultConnection() + }; + return connections; + } + + bool IJsonConnectionStorageService.CanSaveConnection => false; + bool IJsonConnectionStorageService.ContainsConnection(string connectionName) { + return GetConnections().Any(x => x.Name == connectionName); + } + + IEnumerable IJsonConnectionStorageService.GetConnections() { + return GetConnections(); + } + + JsonDataConnection IJsonDataConnectionProviderService.GetJsonDataConnection(string name) { + var connection = GetConnections().FirstOrDefault(x => x.Name == name); + if(connection == null) + throw new InvalidOperationException(); + return connection; + } + + void IJsonConnectionStorageService.SaveConnection(string connectionName, JsonDataConnection connection, bool saveCredentials) { } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomJsonDataConnectionProviderFactory.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomJsonDataConnectionProviderFactory.cs new file mode 100644 index 00000000..1a2cda36 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomJsonDataConnectionProviderFactory.cs @@ -0,0 +1,23 @@ +using DevExpress.DataAccess.Json; +using DevExpress.DataAccess.Web; +namespace EnvelopeGenerator.Server.Client.Services; + +public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory { + public IJsonDataConnectionProviderService Create() { + return new WebDocumentViewerJsonDataConnectionProvider(CustomDataSourceWizardJsonDataConnectionStorage.GetConnections()); + } +} + +public class WebDocumentViewerJsonDataConnectionProvider : IJsonDataConnectionProviderService +{ + readonly List jsonDataConnections; + public WebDocumentViewerJsonDataConnectionProvider(List jsonDataConnections) { + this.jsonDataConnections = jsonDataConnections; + } + public JsonDataConnection GetJsonDataConnection(string name) { + var connection = jsonDataConnections.FirstOrDefault(x => x.Name == name); + if(connection == null) + throw new InvalidOperationException(); + return connection; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomReportProvider.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomReportProvider.cs new file mode 100644 index 00000000..39f3e74d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/CustomReportProvider.cs @@ -0,0 +1,20 @@ +using DevExpress.XtraReports.UI; +using DevExpress.XtraReports.Services; +using EnvelopeGenerator.Server.Client.PredefinedReports; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class CustomReportProvider : IReportProviderAsync { + private readonly InMemoryReportStorageWebExtension reportStorage; + + public CustomReportProvider(InMemoryReportStorageWebExtension reportStorage) { + this.reportStorage = reportStorage; + } + + public Task GetReportAsync(string id, ReportProviderContext context) { + if(reportStorage.TryGetReport(id, out var savedReport)) + return Task.FromResult(savedReport); + + return Task.FromResult(ReportsFactory.GetReport(id)); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocReceiverElementService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocReceiverElementService.cs new file mode 100644 index 00000000..991efecf --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocReceiverElementService.cs @@ -0,0 +1,24 @@ +using System.Net.Http.Json; +using System.Text.Json; +using EnvelopeGenerator.Server.Client.Models; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class DocReceiverElementService(IHttpClientFactory clientFactory) +{ + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + + public async Task> GetAsync(string envelopeKey, CancellationToken cancel = default) + { + var url = $"/api/DocReceiverElement/{Uri.EscapeDataString(envelopeKey)}"; + + var http = clientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await http.GetAsync(url, cancel); + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException($"Failed to retrieve signatures for envelope {envelopeKey}: {response.StatusCode} {response.ReasonPhrase}"); + + var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel); + return result ?? []; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs new file mode 100644 index 00000000..ff649c3c --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs @@ -0,0 +1,32 @@ +using System.Net; +using System.Net.Http; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class DocumentService(IHttpClientFactory httpClientFactory) +{ + + /// + /// Fetches the PDF bytes for the given envelope key from the API. + /// Throws HttpRequestException on failure with appropriate status code. + /// + /// Thrown when the API request fails. + public async Task GetDocumentAsync(string envelopeKey, CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await http.GetAsync($"/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel); + + if (!response.IsSuccessStatusCode) + { + var statusCode = (int)response.StatusCode; + var reasonPhrase = response.ReasonPhrase ?? "Unknown error"; + throw new HttpRequestException( + $"Failed to load document. Status: {statusCode} ({reasonPhrase})", + null, + response.StatusCode); + } + + var bytes = await response.Content.ReadAsByteArrayAsync(cancel); + return bytes; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeReceiverService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeReceiverService.cs new file mode 100644 index 00000000..650f008c --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeReceiverService.cs @@ -0,0 +1,66 @@ +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using EnvelopeGenerator.Application.EnvelopeReceivers.Commands; +using EnvelopeGenerator.Server.Client.Models; + +namespace EnvelopeGenerator.Server.Client.Services; + +/// +/// Retrieves the for the authenticated receiver +/// from GET /api/EnvelopeReceiver/{envelopeKey}. +/// Also creates new envelopes via POST /api/EnvelopeReceiver. +/// +public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory) +{ + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + + /// + /// Fetches the envelope receiver data for the given envelope key from the API. + /// Throws HttpRequestException on failure with appropriate status code. + /// + /// Thrown when the API request fails. + public async Task GetAsync(string envelopeKey, CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var url = $"/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}"; + var response = await http.GetAsync(url, cancel); + + if (!response.IsSuccessStatusCode) + { + var statusCode = (int)response.StatusCode; + var reasonPhrase = response.ReasonPhrase ?? "Unknown error"; + throw new HttpRequestException( + $"Failed to load envelope receiver data. Status: {statusCode} ({reasonPhrase})", + null, + response.StatusCode); + } + + return await response.Content.ReadFromJsonAsync(_jsonOptions, cancel); + } + + /// + /// Creates a new envelope with document and receivers via POST /api/EnvelopeReceiver. + /// Requires sender authentication cookie to be present in the request. + /// + /// Thrown when the API request fails. + public async Task CreateAsync( + CreateEnvelopeReceiverCommand request, + CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await http.PostAsJsonAsync("/api/EnvelopeReceiver", request, _jsonOptions, cancel); + + if (!response.IsSuccessStatusCode) + { + var body = await response.Content.ReadAsStringAsync(cancel); + throw new HttpRequestException( + $"Fehler beim Erstellen des Umschlags. Status: {(int)response.StatusCode} – {body}", + null, + response.StatusCode); + } + + return await response.Content.ReadFromJsonAsync(_jsonOptions, cancel); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs new file mode 100644 index 00000000..69b9f78e --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs @@ -0,0 +1,64 @@ +using EnvelopeGenerator.Application.Common.Dto; +using Microsoft.AspNetCore.WebUtilities; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; + +namespace EnvelopeGenerator.Server.Client.Services; + +/// +/// Retrieves s from the API. +/// +public class EnvelopeService(IHttpClientFactory clientFactory) +{ + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + + /// + /// Fetches envelopes from the API with optional filters. + /// + /// Thrown when the API request fails. + public async Task?> GetAsync( + int? id = null, + string? uuid = null, + bool? onlyActive = null, + bool? onlyCompleted = null, + CancellationToken cancel = default) + { + var baseUrl = $"/api/Envelope"; + var queryParams = new Dictionary(); + + if (id.HasValue) + { + queryParams["Id"] = id.Value.ToString(); + } + if (!string.IsNullOrEmpty(uuid)) + { + queryParams["Uuid"] = uuid; + } + if (onlyActive.HasValue) + { + queryParams["OnlyActive"] = onlyActive.Value.ToString(); + } + if (onlyCompleted.HasValue) + { + queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString(); + } + + var url = QueryHelpers.AddQueryString(baseUrl, queryParams); + + var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await httpClient.GetAsync(url, cancel); + + if (!response.IsSuccessStatusCode) + { + var statusCode = (int)response.StatusCode; + var reasonPhrase = response.ReasonPhrase ?? "Unknown error"; + throw new HttpRequestException( + $"Failed to load envelopes. Status: {statusCode} ({reasonPhrase})", + null, + response.StatusCode); + } + + return await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/FontLoader.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/FontLoader.cs new file mode 100644 index 00000000..96f6ce60 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/FontLoader.cs @@ -0,0 +1,17 @@ +using DevExpress.Drawing; + +namespace EnvelopeGenerator.Server.Client.Services; + +public static class FontLoader +{ + public static async Task LoadFonts(IHttpClientFactory httpClientFactory, List fontNames) + { + using var httpClient = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + + foreach (var fontName in fontNames) + { + var fontBytes = await httpClient.GetByteArrayAsync($"/fonts/{fontName}"); + DXFontRepository.Instance.AddFont(fontBytes); + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/InMemoryReportStorageWebExtension.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/InMemoryReportStorageWebExtension.cs new file mode 100644 index 00000000..246da596 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/InMemoryReportStorageWebExtension.cs @@ -0,0 +1,83 @@ +using DevExpress.XtraReports.UI; +using DevExpress.XtraReports.Web.Extensions; +using EnvelopeGenerator.Server.Client.PredefinedReports; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class InMemoryReportStorageWebExtension : ReportStorageWebExtension +{ + private const string DefaultReportName = "LargeDatasetReport"; + private static readonly Dictionary Reports = new(StringComparer.OrdinalIgnoreCase); + + public override bool CanSetData(string url) => IsValidUrl(url); + + public override byte[] GetData(string url) + { + url = NormalizeUrl(url); + + if (Reports.TryGetValue(url, out var reportLayout)) + return reportLayout; + + if (ReportsFactory.Reports.TryGetValue(url, out var reportFactory)) + return SaveReport(reportFactory()); + + throw new DevExpress.XtraReports.Web.ClientControls.FaultException($"Report '{url}' was not found."); + } + + public override Dictionary GetUrls() + { + var urls = ReportsFactory.Reports.Keys + .Concat(Reports.Keys) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToDictionary(name => name, name => name, StringComparer.OrdinalIgnoreCase); + + return urls; + } + + public override bool IsValidUrl(string url) + { + return !string.IsNullOrWhiteSpace(url) + && url.IndexOfAny(Path.GetInvalidFileNameChars()) < 0; + } + + public override void SetData(XtraReport report, string url) + { + url = NormalizeUrl(url); + Reports[url] = SaveReport(report); + } + + public override string SetNewData(XtraReport report, string defaultUrl) + { + var url = NormalizeUrl(defaultUrl); + Reports[url] = SaveReport(report); + return url; + } + + public bool TryGetReport(string url, out XtraReport report) + { + url = NormalizeUrl(url); + + if (!Reports.ContainsKey(url)) + { + report = null!; + return false; + } + + using var stream = new MemoryStream(Reports[url]); + report = XtraReport.FromXmlStream(stream, true); + report.Name = url; + return true; + } + + private static string NormalizeUrl(string url) + { + return string.IsNullOrWhiteSpace(url) ? DefaultReportName : url; + } + + private static byte[] SaveReport(XtraReport report) + { + using var stream = new MemoryStream(); + report.SaveLayoutToXml(stream); + return stream.ToArray(); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/ObjectDataSourceWizardCustomTypeProvider.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/ObjectDataSourceWizardCustomTypeProvider.cs new file mode 100644 index 00000000..fb83c9c3 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/ObjectDataSourceWizardCustomTypeProvider.cs @@ -0,0 +1,9 @@ +using DevExpress.DataAccess.Web; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class ObjectDataSourceWizardCustomTypeProvider : IObjectDataSourceWizardTypeProvider { + public IEnumerable GetAvailableTypes(string context) { + return new[] { typeof(Data.DataItemList) }; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs new file mode 100644 index 00000000..5478a23d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs @@ -0,0 +1,67 @@ +using System.Net.Http; +using System.Net.Http.Json; +using EnvelopeGenerator.Server.Client.Models; + +namespace EnvelopeGenerator.Server.Client.Services; + +/// +/// Client service for managing cached signatures via API. +/// +public class SignatureCacheService(IHttpClientFactory httpClientFactory) +{ + + public async Task SaveSignatureAsync( + string envelopeKey, + SignatureCaptureDto signature, + CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await http.PostAsJsonAsync( + $"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}", + signature, + cancel); + + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(cancel); + throw new HttpRequestException($"Failed to cache signature: {response.StatusCode} - {error}"); + } + } + + public async Task GetSignatureAsync( + string envelopeKey, + CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await http.GetAsync( + $"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}", + cancel); + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + return null; + + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(cancel); + throw new HttpRequestException($"Failed to retrieve signature: {response.StatusCode} - {error}"); + } + + return await response.Content.ReadFromJsonAsync(cancellationToken: cancel); + } + + public async Task DeleteSignatureAsync( + string envelopeKey, + CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var response = await http.DeleteAsync( + $"/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}", + cancel); + + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(cancel); + throw new HttpRequestException($"Failed to delete signature: {response.StatusCode} - {error}"); + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs new file mode 100644 index 00000000..a8f76d22 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureService.cs @@ -0,0 +1,24 @@ +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using EnvelopeGenerator.Server.Client.Models; + +namespace EnvelopeGenerator.Server.Client.Services; + +public class SignatureService(IHttpClientFactory httpClientFactory) +{ + private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); + + public async Task> GetAsync(string envelopeKey, CancellationToken cancel = default) + { + using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server"); + var url = $"/api/Signature/{Uri.EscapeDataString(envelopeKey)}"; + var response = await http.GetAsync(url, cancel); + + if (!response.IsSuccessStatusCode) + throw new HttpRequestException($"Failed to retrieve signatures for envelope {envelopeKey}: {response.StatusCode} {response.ReasonPhrase}"); + + var result = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancel); + return result ?? []; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/_Imports.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/_Imports.razor new file mode 100644 index 00000000..ed2136be --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/_Imports.razor @@ -0,0 +1,16 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using EnvelopeGenerator.Server.Client +@using EnvelopeGenerator.Server.Client.Services +@using EnvelopeGenerator.Server.Client.Models +@using EnvelopeGenerator.Server.Client.Options +@using DevExpress.Blazor +@using DevExpress.Blazor.PdfViewer +@using DevExpress.Blazor.Reporting diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/AuthScheme.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/AuthScheme.cs new file mode 100644 index 00000000..ecf1cf99 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/AuthScheme.cs @@ -0,0 +1,17 @@ +namespace EnvelopeGenerator.Server; + +/// +/// Authentication scheme names for envelope generator. +/// +public static class AuthScheme +{ + /// + /// Scheme name used for per-envelope receiver JWT authentication. + /// + public const string Receiver = "EnvelopeGenerator.Server.ReceiverJWT"; + + /// + /// Scheme name used for per-envelope sender JWT authentication. + /// + public const string Sender = "EnvelopeGenerator.Server.SenderJWT"; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/App.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/App.razor new file mode 100644 index 00000000..fd5f981b --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/App.razor @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor new file mode 100644 index 00000000..eb4bf201 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor @@ -0,0 +1,1056 @@ +@page "/sender/editor" +@rendermode InteractiveServer +@using DevExpress.Blazor.PdfViewer +@using DevExpress.Blazor.Reporting.Models +@using DevExpress.Blazor +@using EnvelopeGenerator.Application.EnvelopeReceivers.Commands +@using EnvelopeGenerator.Server.Client.Services +@using EnvelopeGenerator.Server.Services +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.Extensions.Caching.Memory +@inject IJSRuntime JSRuntime +@inject NavigationManager NavigationManager +@inject AppVersionService AppVersion +@inject ILogger Logger +@inject EnvelopeReceiverPageDataService ReceiverPageDataService +@inject EnvelopeReceiverService EnvelopeReceiverService +@inject IMemoryCache MemoryCache + + + + + + +
+ + @* ── Action Bar ── *@ +
+
+ + @* Left: Title + meta inputs + receivers *@ +
+ + @* ── Title & Message inputs ── *@ +
+ + @* Titel *@ +
+ + + @if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle)) + { + Pflichtfeld + } +
+ + @* Nachricht *@ +
+ + +
+ + @* PDF filename badge *@ + @if (_pdfLoaded) + { +
+ @_fileName + @if (_signatureFields.Count > 0) + { + + @_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "") + + } +
+ } +
+ +
+
+
+ Empfänger + @_receivers.Count +
+ + +
+ + @if (_receivers.Count == 0) + { +
+ Noch keine Empfänger hinzugefügt. +
+ } + else + { +
+ @foreach (var receiver in _receivers) + { +
+
+
@receiver.FullName
+ + @if (!string.IsNullOrWhiteSpace(receiver.PhoneNumber)) + { +
@receiver.PhoneNumber
+ } +
+ + +
+ } +
+ } +
+
+ + @* Right: Buttons *@ +
+ + + + @* PDF Upload *@ + + + @if (_pdfLoaded) + { + @* Clear all fields *@ + @if (_signatureFields.Count > 0) + { + + } + + @* Save *@ + + } +
+
+ + @* Placement mode hint bar *@ + @if (_pendingReceiverForPlacement is not null) + { +
+ 📌 Klicken Sie auf die Stelle im Dokument für @_pendingReceiverForPlacement.FullName. +   +
+ } +
+ + @* ── Content ── *@ +
+ @if (!_pdfLoaded) + { + @* Empty state *@ +
+
+
+ + + +
+
Kein Dokument geladen
+

+ Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren. +

+ +
+
+ } + else if (_errorMessage is not null) + { +
+
+ Fehler: @_errorMessage +
+
+ } + else + { + @* PDF viewer — click capture active only in placement mode *@ +
+ +
+ } +
+
+ + + +
+
+
+
Vor- und Nachname
+ +
+ +
+
Telefonnummer (optional)
+ +
+ +
+
E-Mail-Adresse
+
+ +
+ +
+ @if (_receiverEmailSuggestions.Count > 0) + { +
+ +
+ } +
+
+
+ +
+ Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen. +
+ + @if (_isReceiverEmailSearchRunning) + { +
E-Mail-Vorschläge werden geladen…
+ } + + @if (!string.IsNullOrWhiteSpace(_receiverPopupValidationMessage)) + { +
@_receiverPopupValidationMessage
+ } +
+
+ + + +
+ +@* ── Save result popup (success + error) ── *@ + + + @if (_saveErrorMessage is null) + { +
+
+ + + +
+
+

+ Umschlag wurde erfolgreich erstellt. +

+

+ Der Umschlag wurde gespeichert und die Empfänger wurden benachrichtigt. +

+
+
+ } + else + { +
+
+ + + + +
+
+

+ Fehler beim Erstellen des Umschlags +

+

+ @_saveErrorMessage +

+
+
+ } +
+ +
+ @if (_saveErrorMessage is null) + { + + } + else + { + + } +
+
+
+ +@code { + // ── Session query param — persists across SignalR reconnects ── + [SupplyParameterFromQuery(Name = "esid")] + public string? Esid { get; set; } + + // ── Constants ── + // Signature field size in PDF points (fixed): 1.77" × 1.96" + const double SigWidthPt = 1.77 * 72; // 127.44 pt + const double SigHeightPt = 1.96 * 72; // 141.12 pt + + // CssClass for DxPdfViewer — used by JS to locate page elements + const string ViewerCssClass = "sender-editor-pdf-viewer"; + + // Cache TTL for editor session (30 min of inactivity) + static readonly TimeSpan SessionTtl = TimeSpan.FromMinutes(30); + + // ── State ── + DxPdfViewer? _pdfViewer; + bool _pdfLoaded = false; + string _fileName = string.Empty; + string? _errorMessage; + byte[]? _pdfBytes; // Current rendered PDF (original + placeholders burned in) + byte[]? _originalPdfBytes; // Pristine upload — never modified, used as base for redraw + + List _signatureFields = []; + ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen" + + // ── Save state ── + bool _isSaving = false; + bool _savePopupVisible = false; + string? _saveErrorMessage = null; + + // ── Envelope metadata ── + string _envelopeTitle = string.Empty; + string _envelopeMessage = string.Empty; + bool _titleTouched = false; + + List _receivers = []; + bool _receiverPopupVisible; + string _receiverDraftName = string.Empty; + string _receiverDraftEmail = string.Empty; + string _receiverDraftPhoneNumber = string.Empty; + string? _selectedReceiverEmailSuggestion; + string? _receiverPopupValidationMessage; + bool _isReceiverEmailSearchRunning; + List _receiverEmailSuggestions = []; + int _receiverEmailSearchVersion; + + static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new(); + + // ── Cache key helper ── + string SessionKey => $"sender-editor:{Esid}"; + + // ── Lifecycle ── + + protected override void OnInitialized() + { + // Intentionally empty. + // esid redirect is done in OnAfterRenderAsync to avoid NavigationException + // during SSR prerendering, where NavigateTo throws by design. + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) return; + + // Generate a session id on first interactive render and push it into the URL. + // At this point SignalR is connected and NavigateTo is safe. + if (string.IsNullOrWhiteSpace(Esid)) + { + var sid = Guid.NewGuid().ToString("N"); + NavigationManager.NavigateTo($"/sender/editor?esid={sid}", forceLoad: false); + } + } + + protected override void OnParametersSet() + { + // After the esid is set via query param, try to restore from cache. + if (!string.IsNullOrWhiteSpace(Esid) + && MemoryCache.TryGetValue(SessionKey, out EditorSessionData? cached) + && cached is not null) + { + _originalPdfBytes = cached.OriginalPdfBytes; + _signatureFields = cached.Fields; + _fileName = cached.FileName; + _pdfLoaded = _originalPdfBytes is { Length: > 0 }; + _receivers = cached.Receivers; + _envelopeTitle = cached.Title; + _envelopeMessage = cached.Message; + + // Redraw placeholders onto the original PDF + if (_pdfLoaded) + _pdfBytes = DrawPlaceholders(_originalPdfBytes!, _signatureFields); + } + } + + // ── PDF upload ── + async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e) + { + _errorMessage = null; + var file = e.File; + if (file is null) return; + + if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) + { + _errorMessage = "Bitte wählen Sie eine PDF-Datei aus."; + return; + } + + try + { + const long maxBytes = 50 * 1024 * 1024; + using var ms = new System.IO.MemoryStream(); + await file.OpenReadStream(maxBytes).CopyToAsync(ms); + + _originalPdfBytes = ms.ToArray(); + _fileName = file.Name; + _pdfLoaded = true; + _signatureFields.Clear(); + _pendingReceiverForPlacement = null; + + // Rendered PDF starts as the clean original (no placeholders yet) + _pdfBytes = _originalPdfBytes; + + PersistSession(); + + Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _originalPdfBytes.Length); + } + catch (Exception ex) + { + _errorMessage = $"Fehler beim Laden der Datei: {ex.Message}"; + Logger.LogError(ex, "Failed to load PDF file"); + } + } + + // ── Placement mode ── + void ActivatePlacementForReceiver(ReceiverDraft receiver) + { + // Toggle: clicking the same receiver again cancels placement + _pendingReceiverForPlacement = _pendingReceiverForPlacement?.Id == receiver.Id + ? null + : receiver; + } + + void CancelPlacement() => _pendingReceiverForPlacement = null; + + // ── PDF area click → place field ── + async Task OnPdfAreaClickAsync(MouseEventArgs e) + { + if (_pendingReceiverForPlacement is null) return; + if (!_pdfLoaded || _originalPdfBytes is null) return; + + // Ask JS for the normalised click position within the rendered PDF page + var coords = await JSRuntime.InvokeAsync( + "envelopeEditor.getClickCoordsOnPdfPage", + ViewerCssClass, e.ClientX, e.ClientY); + + if (coords is null) + { + Logger.LogWarning("[SenderEditor] getClickCoordsOnPdfPage returned null"); + return; + } + + // Read page dimensions from the original PDF via PdfSharp + double pageWidthPt; + double pageHeightPt; + try + { + using var ms = new System.IO.MemoryStream(_originalPdfBytes); + var doc = PdfSharp.Pdf.IO.PdfReader.Open(ms, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Import); + int pageIndex = Math.Max(0, Math.Min(coords.PageIndex, doc.PageCount - 1)); + var page = doc.Pages[pageIndex]; + pageWidthPt = page.Width.Point; + pageHeightPt = page.Height.Point; + } + catch (Exception ex) + { + Logger.LogError(ex, "[SenderEditor] Failed to read page dimensions from PDF"); + return; + } + + // Convert normalised [0,1] → PDF points; clamp so box stays inside page + double xPt = coords.NormX * pageWidthPt; + double yPt = coords.NormY * pageHeightPt; + + xPt = Math.Max(0, Math.Min(xPt, pageWidthPt - SigWidthPt)); + yPt = Math.Max(0, Math.Min(yPt, pageHeightPt - SigHeightPt)); + + int page1Based = coords.PageIndex + 1; + + var field = new SignatureFieldDraft( + XPt: xPt, + YPt: yPt, + Page: page1Based, + ReceiverName: _pendingReceiverForPlacement.FullName, + Color: _pendingReceiverForPlacement.Color); + + _signatureFields.Add(field); + _pendingReceiverForPlacement = null; + + // Burn all placeholders onto the original PDF and update the viewer + _pdfBytes = DrawPlaceholders(_originalPdfBytes, _signatureFields); + PersistSession(); + + Logger.LogInformation( + "[SenderEditor] Field added: Page={Page} X={X:F1}pt Y={Y:F1}pt Receiver={Receiver}", + page1Based, xPt, yPt, field.ReceiverName); + } + + // ── Remove a single field ── + async Task RemoveFieldAsync(SignatureFieldDraft field) + { + _signatureFields.Remove(field); + _pdfBytes = _originalPdfBytes is not null + ? DrawPlaceholders(_originalPdfBytes, _signatureFields) + : _pdfBytes; + PersistSession(); + await Task.CompletedTask; + } + + // ── Clear all fields ── + async Task ClearAllFieldsAsync() + { + _signatureFields.Clear(); + _pendingReceiverForPlacement = null; + _pdfBytes = _originalPdfBytes; + PersistSession(); + await Task.CompletedTask; + } + + void Cancel() => NavigationManager.NavigateTo("/sender"); + + void GoToDashboard() + { + // Clear session from cache on successful save so it is not restored if user returns + if (!string.IsNullOrWhiteSpace(Esid)) + MemoryCache.Remove(SessionKey); + + NavigationManager.NavigateTo("/sender"); + } + + // ── Save — POST /api/EnvelopeReceiver ── + async Task SaveAsync() + { + // ── Validation ── + _titleTouched = true; + if (!_pdfLoaded || _originalPdfBytes is null) + { + _saveErrorMessage = "Bitte laden Sie zuerst ein PDF-Dokument hoch."; + _savePopupVisible = true; + return; + } + if (string.IsNullOrWhiteSpace(_envelopeTitle)) + { + _saveErrorMessage = "Bitte geben Sie einen Titel ein."; + _savePopupVisible = true; + return; + } + if (_receivers.Count == 0) + { + _saveErrorMessage = "Bitte fügen Sie mindestens einen Empfänger hinzu."; + _savePopupVisible = true; + return; + } + if (_signatureFields.Count == 0) + { + _saveErrorMessage = "Bitte platzieren Sie mindestens ein Signaturfeld."; + _savePopupVisible = true; + return; + } + + // Warn if any receiver has no signature field, but don't block + var receiversWithoutField = _receivers + .Where(r => !_signatureFields.Any(f => f.ReceiverName == r.FullName)) + .ToList(); + if (receiversWithoutField.Count > 0) + { + Logger.LogWarning( + "[SenderEditor] Receivers without signature field: {Names}", + string.Join(", ", receiversWithoutField.Select(r => r.FullName))); + } + + _isSaving = true; + _saveErrorMessage = null; + await InvokeAsync(StateHasChanged); + + try + { + // ── Build request ── + // Document: use the ORIGINAL pdf (without placeholder burn-in) as base64 + var docBase64 = Convert.ToBase64String(_originalPdfBytes); + + // Build receivers list — each receiver gets their own signature fields + // Coordinate conversion: PDF points → inches (DB stores inches) + var receiversCmd = _receivers.Select(receiver => + { + var fields = _signatureFields + .Where(f => f.ReceiverName == receiver.FullName) + .Select(f => new DocReceiverElementCreateDto( + X: f.XPt / 72.0, + Y: f.YPt / 72.0, + Page: f.Page)) + .ToList(); + + return new ReceiverGetOrCreateCommand + { + EmailAddress = receiver.Email, + Salution = receiver.FullName, + PhoneNumber = string.IsNullOrWhiteSpace(receiver.PhoneNumber) + ? null + : receiver.PhoneNumber, + DocReceiverElements = fields, + }; + }).ToList(); + + var command = new CreateEnvelopeReceiverCommand + { + Title = _envelopeTitle.Trim(), + Message = string.IsNullOrWhiteSpace(_envelopeMessage) + ? "Bitte unterzeichnen Sie das beigefügte Dokument." + : _envelopeMessage.Trim(), + TFAEnabled = false, + Document = new DocumentCreateCommand { DataAsBase64 = docBase64 }, + Receivers = receiversCmd, + }; + + var result = await EnvelopeReceiverService.CreateAsync(command); + + Logger.LogInformation( + "[SenderEditor] Envelope created. Id={Id} SentReceivers={Count}", + result?.Id, result?.SentReceiver.Count()); + + // Success — show popup; GoToDashboard clears cache and navigates + _saveErrorMessage = null; + _savePopupVisible = true; + } + catch (Exception ex) + { + Logger.LogError(ex, "[SenderEditor] Failed to create envelope"); + _saveErrorMessage = ex.Message; + _savePopupVisible = true; + } + finally + { + _isSaving = false; + } + } + + // ── Cache persistence ── + void PersistSession() + { + if (string.IsNullOrWhiteSpace(Esid)) return; + + var data = new EditorSessionData( + OriginalPdfBytes: _originalPdfBytes ?? [], + Fields: [.. _signatureFields], + FileName: _fileName, + Receivers: [.. _receivers], + Title: _envelopeTitle, + Message: _envelopeMessage); + + MemoryCache.Set(SessionKey, data, SessionTtl); + } + + // ── PdfSharp: burn all placeholder boxes onto the original PDF ── + static byte[] DrawPlaceholders(byte[] originalPdf, IReadOnlyList fields) + { + if (fields.Count == 0) return originalPdf; + + using var inputMs = new System.IO.MemoryStream(originalPdf); + using var outputMs = new System.IO.MemoryStream(); + + var document = PdfSharp.Pdf.IO.PdfReader.Open( + inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify); + + foreach (var field in fields) + { + int pageIndex = field.Page - 1; + if (pageIndex < 0 || pageIndex >= document.PageCount) continue; + + var page = document.Pages[pageIndex]; + using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page); + + // Derive colours from the receiver's hex colour + var fillBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 35)); + var borderPen = new PdfSharp.Drawing.XPen(HexToXColor(field.Color, alpha: 210), 1.5); + var textBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 200)); + var nameBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 230)); + + var fontLabel = new PdfSharp.Drawing.XFont("Arial", 9, PdfSharp.Drawing.XFontStyleEx.Bold); + var fontName = new PdfSharp.Drawing.XFont("Arial", 7, PdfSharp.Drawing.XFontStyleEx.Regular); + + var fmtCenter = new PdfSharp.Drawing.XStringFormat + { + Alignment = PdfSharp.Drawing.XStringAlignment.Center, + LineAlignment = PdfSharp.Drawing.XLineAlignment.Center, + }; + + var rect = new PdfSharp.Drawing.XRect(field.XPt, field.YPt, SigWidthPt, SigHeightPt); + + gfx.DrawRectangle(fillBrush, rect); + gfx.DrawRectangle(borderPen, rect); + + // "UNTERSCHRIFT" label centred in upper two-thirds + var labelRect = new PdfSharp.Drawing.XRect( + field.XPt, field.YPt, SigWidthPt, SigHeightPt * 0.65); + gfx.DrawString("UNTERSCHRIFT", fontLabel, textBrush, labelRect, fmtCenter); + + // Receiver name centred in lower third + var nameRect = new PdfSharp.Drawing.XRect( + field.XPt + 4, field.YPt + SigHeightPt * 0.68, + SigWidthPt - 8, SigHeightPt * 0.30); + var displayName = field.ReceiverName.Length > 22 + ? field.ReceiverName[..19] + "..." + : field.ReceiverName; + gfx.DrawString(displayName, fontName, nameBrush, nameRect, fmtCenter); + } + + document.Save(outputMs); + return outputMs.ToArray(); + } + + /// Converts a CSS hex colour string (e.g. "#4F46E5") to a PdfSharp XColor. + static PdfSharp.Drawing.XColor HexToXColor(string hex, int alpha) + { + var c = System.Drawing.ColorTranslator.FromHtml(hex); + return PdfSharp.Drawing.XColor.FromArgb(alpha, c.R, c.G, c.B); + } + + // ── Receiver popup ── + void OpenAddReceiverPopup() + { + _receiverDraftName = string.Empty; + _receiverDraftEmail = string.Empty; + _receiverDraftPhoneNumber = string.Empty; + _selectedReceiverEmailSuggestion = null; + _receiverPopupValidationMessage = null; + _receiverEmailSuggestions.Clear(); + _receiverPopupVisible = true; + } + + void CloseAddReceiverPopup() + { + _receiverPopupVisible = false; + _receiverPopupValidationMessage = null; + _selectedReceiverEmailSuggestion = null; + _isReceiverEmailSearchRunning = false; + } + + void OnReceiverNameChanged(string? value) + { + _receiverDraftName = value ?? string.Empty; + _receiverPopupValidationMessage = null; + } + + void OnReceiverPhoneNumberChanged(string? value) + { + _receiverDraftPhoneNumber = value ?? string.Empty; + _receiverPopupValidationMessage = null; + } + + async Task OnReceiverEmailKeyDownAsync(KeyboardEventArgs e) + { + if (_receiverEmailSuggestions.Count == 0) + { + if (e.Key == "Escape") _selectedReceiverEmailSuggestion = null; + return; + } + + var currentIndex = _selectedReceiverEmailSuggestion is null + ? -1 + : _receiverEmailSuggestions.FindIndex(em => + string.Equals(em, _selectedReceiverEmailSuggestion, StringComparison.OrdinalIgnoreCase)); + + if (e.Key == "ArrowDown") + { + var next = currentIndex < _receiverEmailSuggestions.Count - 1 ? currentIndex + 1 : 0; + SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]); + } + else if (e.Key == "ArrowUp") + { + var next = currentIndex > 0 ? currentIndex - 1 : _receiverEmailSuggestions.Count - 1; + SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]); + } + else if (e.Key == "Enter") + { + var sel = currentIndex >= 0 && currentIndex < _receiverEmailSuggestions.Count + ? _receiverEmailSuggestions[currentIndex] + : _receiverEmailSuggestions.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(sel)) + await OnReceiverEmailSuggestionCommittedAsync(sel); + } + else if (e.Key == "Escape") + { + _receiverEmailSuggestions.Clear(); + _selectedReceiverEmailSuggestion = null; + } + } + + void SelectReceiverEmailSuggestion(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return; + _selectedReceiverEmailSuggestion = value.Trim(); + _receiverDraftEmail = _selectedReceiverEmailSuggestion; + _receiverPopupValidationMessage = null; + } + + Task OnReceiverEmailSuggestionCommittedAsync(string? value) + { + SelectReceiverEmailSuggestion(value); + _receiverEmailSuggestions.Clear(); + return Task.CompletedTask; + } + + async Task OnReceiverEmailTextChangedAsync(string? value) + { + _receiverDraftEmail = value?.Trim() ?? string.Empty; + _selectedReceiverEmailSuggestion = _receiverDraftEmail; + _receiverPopupValidationMessage = null; + + var searchVersion = ++_receiverEmailSearchVersion; + + if (string.IsNullOrWhiteSpace(_receiverDraftEmail) || _receiverDraftEmail.Length < 2) + { + _receiverEmailSuggestions.Clear(); + _selectedReceiverEmailSuggestion = null; + _isReceiverEmailSearchRunning = false; + return; + } + + _isReceiverEmailSearchRunning = true; + + try + { + var results = await ReceiverPageDataService.SearchReceiverEMailsAsync(_receiverDraftEmail); + if (searchVersion != _receiverEmailSearchVersion) return; + + _receiverEmailSuggestions = results + .Where(em => !string.IsNullOrWhiteSpace(em)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(em => em) + .Take(12) + .ToList(); + + _selectedReceiverEmailSuggestion = _receiverEmailSuggestions.FirstOrDefault(em => + string.Equals(em, _receiverDraftEmail, StringComparison.OrdinalIgnoreCase)); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to load receiver email suggestions for {SearchTerm}", _receiverDraftEmail); + if (searchVersion == _receiverEmailSearchVersion) + { + _receiverEmailSuggestions.Clear(); + _selectedReceiverEmailSuggestion = null; + } + } + finally + { + if (searchVersion == _receiverEmailSearchVersion) + _isReceiverEmailSearchRunning = false; + } + } + + Task SaveReceiverAsync() + { + var fullName = _receiverDraftName.Trim(); + var email = _receiverDraftEmail.Trim(); + var phoneNumber = _receiverDraftPhoneNumber.Trim(); + + if (string.IsNullOrWhiteSpace(fullName)) + { + _receiverPopupValidationMessage = "Bitte geben Sie einen Vor- und Nachnamen ein."; + return Task.CompletedTask; + } + if (string.IsNullOrWhiteSpace(email)) + { + _receiverPopupValidationMessage = "Bitte geben Sie eine E-Mail-Adresse ein."; + return Task.CompletedTask; + } + if (!ReceiverEmailValidator.IsValid(email)) + { + _receiverPopupValidationMessage = "Bitte geben Sie eine gültige E-Mail-Adresse ein."; + return Task.CompletedTask; + } + if (_receivers.Any(r => string.Equals(r.Email, email, StringComparison.OrdinalIgnoreCase))) + { + _receiverPopupValidationMessage = "Diese E-Mail-Adresse wurde bereits hinzugefügt."; + return Task.CompletedTask; + } + + _receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email, phoneNumber, + ReceiverPalette[_receivers.Count % ReceiverPalette.Length])); + PersistSession(); + CloseAddReceiverPopup(); + return Task.CompletedTask; + } + + // ── Models ── + record SignatureFieldDraft(double XPt, double YPt, int Page, string ReceiverName, string Color); + + record NormalisedCoords(double NormX, double NormY, int PageIndex); + + record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber, string Color); + + record EditorSessionData( + byte[] OriginalPdfBytes, + List Fields, + string FileName, + List Receivers, + string Title, + string Message); + + // ── Receiver colour palette (cycles when > 8 receivers) ── + static readonly string[] ReceiverPalette = + [ + "#4F46E5", // indigo + "#059669", // emerald + "#DC2626", // red + "#D97706", // amber + "#7C3AED", // violet + "#0891B2", // cyan + "#BE185D", // pink + "#65A30D", // lime + ]; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Error.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Error.razor new file mode 100644 index 00000000..576cc2d2 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor new file mode 100644 index 00000000..1efb9222 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverPage.razor @@ -0,0 +1,723 @@ +@page "/envelope/{EnvelopeKey}" +@rendermode InteractiveServer +@using DevExpress.Blazor.Reporting +@using DevExpress.XtraReports.UI +@using EnvelopeGenerator.Server.Client.Models +@using EnvelopeGenerator.Server.Client.Models.Constants +@using EnvelopeGenerator.Server.Client.Services +@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver +@using Microsoft.JSInterop +@using DevExpress.Blazor +@using System.Drawing +@using System.Security.Claims +@using Microsoft.Extensions.Caching.Memory +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService +@inject AppVersionService AppVersion +@inject IMemoryCache MemoryCache +@inject ILogger Logger +@implements IDisposable + + + + + + +
+
+
+ @* Row 1: Title + Sender + Badges *@ +
+ @* Left: Title + Sender *@ +
+ @if (_envelopeReceiver is not null) + { +
+ @(_envelopeReceiver.Envelope?.Title ?? "Dokument") +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) + { + + Von + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) + { + @_envelopeReceiver.Envelope.User.FullName + } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) + { + <@_envelopeReceiver.Envelope.User.Email> + } + @if (_envelopeReceiver.Envelope?.AddedWhen != null) + { +  · @_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy") + } + + } + } + else + { +
Dokumentenansicht
+ } +
+ + @* Right: Badges + Signature status *@ +
+ @if (_envelopeReceiver is not null) + { +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) + { + + + + + @_envelopeReceiver.Name + + } + @if (_signatures.Count > 0) + { + + + + + @_signatures.Count Unterschrift@(_signatures.Count != 1 ? "en" : "") + @if (_capturedSignature is not null) + { + + } + + } + @if (_envelopeReceiver.Envelope?.UseAccessCode ?? false) + { + + + + + Code + + } + @if (_envelopeReceiver.Envelope?.TFAEnabled ?? false) + { + + + + + + 2FA + + } +
+ } + + @* Unterschreiben button — visible only when signature fields exist *@ + @if (_signatures.Count > 0) + { + + } +
+
+ + @* Row 2: Messages *@ + @if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))) + { +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) + { +
+ 📧 + @_envelopeReceiver.Envelope.Message +
+ } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) + { +
+ 🔒 + @_envelopeReceiver.PrivateMessage +
+ } +
+ } +
+
+ +
+ @if (_isLoading) + { +
+
+
+ Lädt... +
+

Dokument wird geladen...

+
+
+ } + else if (_errorMessage is not null) + { +
+
+
+ + + + +
+
Fehler beim Laden des Dokuments
+

@_errorMessage

+
+
+
+
+ } + else if (_report is not null) + { + + } +
+
+ +@* Signature Popup *@ + + + + + @if (_activeSignatureTab == SignatureTabDraw) + { +

Bitte unterschreiben Sie im folgenden Feld.

+ + } + else if (_activeSignatureTab == SignatureTabText) + { +

Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.

+
+
+ +
+
+ +
+
+ + } + else + { +

Laden Sie ein Bild Ihrer Unterschrift hoch.

+ + + } + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + @if (!string.IsNullOrWhiteSpace(_popupValidationMessage)) + { +
+ @_popupValidationMessage +
+ } +
+ +
+ + +
+
+
+ +@code { + // ----- Constants ----- + const string SignatureTabDraw = "draw"; + const string SignatureTabText = "text"; + const string SignatureTabImage = "image"; + const string DrawCanvasId = "rp-signature-pad"; + const string TypedCanvasId = "rp-typed-signature-pad"; + const string ImageInputId = "rp-signature-image-input"; + const string ImageCanvasId = "rp-image-signature-pad"; + + readonly (string Text, string Value)[] TypedSignatureFonts = + [ + ("Brush Script", "'Brush Script MT', cursive"), + ("Segoe Script", "'Segoe Script', cursive"), + ("Lucida Handwriting", "'Lucida Handwriting', cursive"), + ("Comic Sans", "'Comic Sans MS', cursive"), + ("Cursive", "cursive"), + ]; + + // ----- Parameters ----- + [Parameter] public string? EnvelopeKey { get; set; } + + // ----- Page state ----- + bool _isLoading = true; + string? _errorMessage; + byte[]? _pdfBytes; + IReadOnlyList _signatures = []; + EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; + ClaimsPrincipal? _receiverUser; + + // ----- Report viewer ----- + DxReportViewer? _reportViewer; + XtraReport? _report; + + // ----- Signature popup state ----- + SignatureCaptureDto? _capturedSignature; + bool _signaturePopupVisible = false; + string? _popupValidationMessage; + string _activeSignatureTab = SignatureTabDraw; + string _typedSignatureText = string.Empty; + string _typedSignatureFont = "'Brush Script MT', cursive"; + string _signerFullName = string.Empty; + string _signerPosition = string.Empty; + string _signaturePlace = string.Empty; + + // ----- Lifecycle ----- + protected override async Task OnInitializedAsync() + { + if (string.IsNullOrWhiteSpace(EnvelopeKey)) + { + _errorMessage = "Envelope-Schlüssel fehlt."; + _isLoading = false; + return; + } + + // Authorization — same pattern as EnvelopeReceiverPage + _receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); + if (_receiverUser is null) + { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } + + try + { + // Load PDF bytes via MediatR (uses authenticated user's claims) + _pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); + if (_pdfBytes is not { Length: > 0 }) + { + _errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen."; + _isLoading = false; + return; + } + + // Load signature fields for this receiver + _signatures = await PageDataService.GetSignaturesAsync(_receiverUser); + + // Load envelope receiver metadata + _envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); + if (_envelopeReceiver is null) + Logger.LogWarning("Envelope receiver data is null for {EnvelopeKey}", EnvelopeKey); + + // Build initial report (no signature image yet) + _report = BuildReport(_pdfBytes, _signatures, capturedSignature: null); + + // Try to restore cached signature + try + { + var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser); + if (cachedSignature is not null) + { + _capturedSignature = cachedSignature; + _signerFullName = cachedSignature.FullName; + _signerPosition = cachedSignature.Position; + _signaturePlace = cachedSignature.Place; + _signaturePopupVisible = false; + + // Rebuild with cached signature overlaid + _report = BuildReport(_pdfBytes, _signatures, _capturedSignature); + } + else + { + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = false; + _popupValidationMessage = null; + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to load cached signature for {EnvelopeKey}", EnvelopeKey); + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = false; + _popupValidationMessage = null; + } + } + catch (Exception ex) + { + _errorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}"; + Logger.LogError(ex, "Unexpected error for {EnvelopeKey}", EnvelopeKey); + } + + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + + // ----- Report builder ----- + /// + /// Builds an XtraReport wrapping the PDF bytes. + /// If a signature is captured and there are signature fields, the signature image is + /// first burned into the PDF via DevExpress PdfDocumentProcessor, then the modified + /// PDF is handed to XRPdfContent with GenerateOwnPages = true so that all pages appear. + /// + static XtraReport BuildReport( + byte[] pdfBytes, + IReadOnlyList signatures, + SignatureCaptureDto? capturedSignature) + { + // Always draw placeholder boxes on signature fields so the user knows where to sign. + // When a captured signature exists, it will be applied in the Signed page instead. + byte[] sourcePdf = pdfBytes; + if (signatures.Count > 0) + { + sourcePdf = DrawSignaturePlaceholders(pdfBytes, signatures); + } + + var report = new XtraReport + { + PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4, + Landscape = false, + Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0), + }; + + var detail = new DetailBand { HeightF = 0f }; + report.Bands.Add(detail); + + detail.Controls.Add(new XRPdfContent + { + Source = sourcePdf, + GenerateOwnPages = true, + }); + + return report; + } + + /// + /// Uses PdfSharp to draw a visible signature placeholder box on every signature field. + /// sig.X / sig.Y come from GetSignaturesAsync(UnitOfLength.Point) → already in PDF points. + /// PdfSharp coordinate origin: bottom-left, Y up. Conversion: pdfY = pageH - sigY - sigH + /// Signature field size (fixed): 1.77" × 1.96" = 127.44pt × 141.12pt + /// + static byte[] DrawSignaturePlaceholders( + byte[] pdfBytes, + IReadOnlyList signatures) + { + if (signatures.Count == 0) return pdfBytes; + + using var inputMs = new System.IO.MemoryStream(pdfBytes); + using var outputMs = new System.IO.MemoryStream(); + + var document = PdfSharp.Pdf.IO.PdfReader.Open( + inputMs, + PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify); + + const double sigW = 1.77 * 72; // 127.44 pt + const double sigH = 1.96 * 72; // 141.12 pt + + foreach (var sig in signatures) + { + int pageIndex = sig.Page - 1; + if (pageIndex < 0 || pageIndex >= document.PageCount) continue; + + var page = document.Pages[pageIndex]; + + // PdfSharp XGraphics uses top-left origin, Y down — same as sig.X/sig.Y + // No coordinate conversion needed. + using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page); + + var rect = new PdfSharp.Drawing.XRect(sig.X, sig.Y, sigW, sigH); + + // Filled semi-transparent rectangle + var fillBrush = new PdfSharp.Drawing.XSolidBrush( + PdfSharp.Drawing.XColor.FromArgb(40, 60, 80, 160)); + var borderPen = new PdfSharp.Drawing.XPen( + PdfSharp.Drawing.XColor.FromArgb(200, 60, 80, 200), 1.5); + + gfx.DrawRectangle(fillBrush, rect); + gfx.DrawRectangle(borderPen, rect); + + // "UNTERSCHRIFT" label centred in the box + var font = new PdfSharp.Drawing.XFont("Arial", 9, + PdfSharp.Drawing.XFontStyleEx.Bold); + var textBrush = new PdfSharp.Drawing.XSolidBrush( + PdfSharp.Drawing.XColor.FromArgb(200, 40, 60, 140)); + + var textFmt = new PdfSharp.Drawing.XStringFormat + { + Alignment = PdfSharp.Drawing.XStringAlignment.Center, + LineAlignment = PdfSharp.Drawing.XLineAlignment.Center, + }; + gfx.DrawString("UNTERSCHRIFT", font, textBrush, rect, textFmt); + } + + document.Save(outputMs); + return outputMs.ToArray(); + } + + /// Converts a base64 data URL (data:image/...;base64,...) to raw bytes. + static byte[]? DataUrlToBytes(string dataUrl) + { + try + { + var commaIndex = dataUrl.IndexOf(','); + if (commaIndex < 0) return null; + return Convert.FromBase64String(dataUrl[(commaIndex + 1)..]); + } + catch + { + return null; + } + } + + // ----- Signature popup handlers ----- + void OpenSignaturePopup() + { + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + _popupValidationMessage = null; + } + + async Task OnPopupShownAsync() + { + await InitializeActiveSignatureTabAsync(); + } + + async Task SetSignatureTabAsync(string tab) + { + _activeSignatureTab = tab; + _popupValidationMessage = null; + await InvokeAsync(StateHasChanged); + await Task.Delay(50); + await InitializeActiveSignatureTabAsync(); + } + + async Task InitializeActiveSignatureTabAsync() + { + if (_activeSignatureTab == SignatureTabDraw) + await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId); + else if (_activeSignatureTab == SignatureTabText) + { + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId); + await RenderTypedSignatureAsync(); + } + else + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId); + } + + async Task RenewSignatureAsync() + { + _popupValidationMessage = null; + if (_activeSignatureTab == SignatureTabDraw) + await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId); + else if (_activeSignatureTab == SignatureTabText) + { + _typedSignatureText = string.Empty; + await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId); + } + else + await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId); + } + + async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) + { + _typedSignatureText = args.Value?.ToString() ?? string.Empty; + await RenderTypedSignatureAsync(); + } + + async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) + { + _typedSignatureFont = args.Value?.ToString() ?? _typedSignatureFont; + await RenderTypedSignatureAsync(); + } + + async Task RenderTypedSignatureAsync() + { + await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", + TypedCanvasId, _typedSignatureText, _typedSignatureFont); + } + + async Task SaveSignatureAsync() + { + if (string.IsNullOrWhiteSpace(_signerFullName)) + { + _popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein."; + return; + } + if (string.IsNullOrWhiteSpace(_signaturePlace)) + { + _popupValidationMessage = "Bitte geben Sie den Ort ein."; + return; + } + + var signatureDataUrl = await GetActiveSignatureDataUrlAsync(); + if (string.IsNullOrWhiteSpace(signatureDataUrl)) + { + _popupValidationMessage = "Die Unterschrift ist erforderlich."; + return; + } + + _popupValidationMessage = null; + _capturedSignature = new SignatureCaptureDto + { + DataUrl = signatureDataUrl, + FullName = _signerFullName.Trim(), + Position = _signerPosition.Trim(), + Place = _signaturePlace.Trim(), + }; + _signaturePopupVisible = false; + + // Store signature in IMemoryCache with a Guid key (1 minute TTL) + var sid = Guid.NewGuid().ToString("N"); + MemoryCache.Set( + sid, + _capturedSignature, + TimeSpan.FromMinutes(1)); + + Logger.LogInformation( + "Signature cached with sid={Sid} for envelope {EnvelopeKey}", sid, EnvelopeKey); + + // Null the report → DxReportViewer removed from DOM → no crash on dispose + _report = null; + await InvokeAsync(StateHasChanged); + await Task.Delay(50); + + // Navigate — forceLoad:true for clean circuit teardown + Navigation.NavigateTo( + $"/envelope/{Uri.EscapeDataString(EnvelopeKey!)}/signed?sid={sid}", + forceLoad: true); + } + + async Task GetActiveSignatureDataUrlAsync() + { + if (_activeSignatureTab == SignatureTabDraw) + return await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", DrawCanvasId); + + if (_activeSignatureTab == SignatureTabText) + { + await RenderTypedSignatureAsync(); + return await JSRuntime.InvokeAsync("receiverSignature.getTypedDataUrl", TypedCanvasId); + } + + return await JSRuntime.InvokeAsync("receiverSignature.getImageDataUrl", ImageCanvasId); + } + + // ----- Disposal ----- + public void Dispose() + { + _report?.Dispose(); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor new file mode 100644 index 00000000..f5b886dd --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/ReceiverSignedPage.razor @@ -0,0 +1,396 @@ +@page "/envelope/{EnvelopeKey}/signed" +@rendermode InteractiveServer +@using DevExpress.Blazor.Reporting +@using DevExpress.XtraReports.UI +@using EnvelopeGenerator.Server.Client.Models +@using EnvelopeGenerator.Server.Client.Services +@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver +@using Microsoft.Extensions.Caching.Memory +@using System.Security.Claims +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService +@inject AppVersionService AppVersion +@inject IMemoryCache MemoryCache +@inject ILogger Logger +@implements IDisposable + + + + + +
+
+
+
+
+ @if (_envelopeReceiver is not null) + { +
+ @(_envelopeReceiver.Envelope?.Title ?? "Dokument") +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) + { + + Von @_envelopeReceiver.Envelope.User.FullName + + } + } + else + { +
Signiertes Dokument
+ } +
+ + @* Right: Submit button *@ +
+ +
+
+
+
+ +
+ @if (_isLoading) + { +
+
+
+ Lädt... +
+

Dokument wird geladen...

+
+
+ } + else if (_errorMessage is not null) + { +
+
+
+ + + + +
+
Fehler
+

@_errorMessage

+
+
+
+
+ } + else if (_report is not null) + { + + } +
+
+ +@* Submit confirmation popup *@ + + +
+
+ + + +
+
+

+ Möchten Sie das Dokument verbindlich unterschreiben? +

+

+ Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt. +

+
+
+
+ +
+ + +
+
+
+ +@code { + [Parameter] public string? EnvelopeKey { get; set; } + + [SupplyParameterFromQuery(Name = "sid")] + public string? Sid { get; set; } + + bool _isLoading = true; + string? _errorMessage; + ClaimsPrincipal? _receiverUser; + EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; + IReadOnlyList _signatures = []; + XtraReport? _report; + SignatureCaptureDto? _sig; + + // ----- Submit / logout state ----- + bool _isLoggingOut = false; + bool _submitConfirmVisible = false; + + void OpenSubmitConfirmPopup() => _submitConfirmVisible = true; + + async Task SubmitAndLogoutAsync() + { + if (_isLoggingOut) return; + _isLoggingOut = true; + _submitConfirmVisible = false; + await InvokeAsync(StateHasChanged); + await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!); + Navigation.NavigateTo("/", forceLoad: true); + } + + protected override async Task OnInitializedAsync() + { + if (string.IsNullOrWhiteSpace(EnvelopeKey)) + { + _errorMessage = "Envelope-Schlüssel fehlt."; + _isLoading = false; + return; + } + + _receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); + if (_receiverUser is null) + { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } + + // Read signature from IMemoryCache + if (!string.IsNullOrWhiteSpace(Sid) + && MemoryCache.TryGetValue(Sid, out SignatureCaptureDto? cached) + && cached is not null) + { + _sig = cached; + } + + // Cache miss or missing sid — redirect back to report page + if (_sig is null) + { + Logger.LogWarning( + "[SignedPage] Cache miss or no sid={Sid} for {EnvelopeKey}, redirecting to report page.", + Sid, EnvelopeKey); + Navigation.NavigateTo( + $"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", + forceLoad: true); + return; + } + + try + { + var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); + if (pdfBytes is not { Length: > 0 }) + { + _errorMessage = "Dokument konnte nicht geladen werden."; + _isLoading = false; + return; + } + + _envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); + _signatures = await PageDataService.GetSignaturesAsync(_receiverUser); + + // Burn signature image + info onto PDF via PdfSharp + if (_sig is not null && _signatures.Count > 0) + pdfBytes = DrawSignaturesOnPdf(pdfBytes, _signatures, _sig); + + var report = new XtraReport + { + PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4, + Landscape = false, + Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0), + }; + var detail = new DetailBand(); + report.Bands.Add(detail); + detail.Controls.Add(new XRPdfContent + { + Source = pdfBytes, + GenerateOwnPages = true, + }); + _report = report; + } + catch (Exception ex) + { + _errorMessage = $"Fehler: {ex.Message}"; + Logger.LogError(ex, "Error loading signed page for {EnvelopeKey}", EnvelopeKey); + } + + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) return; + + if (_sig is not null) + { + await JSRuntime.InvokeVoidAsync("console.log", + $"[SignedPage] sid={Sid} | FullName={_sig.FullName} | Place={_sig.Place} | Position={_sig.Position} | DataUrl.Length={_sig.DataUrl?.Length ?? 0}"); + } + else + { + await JSRuntime.InvokeVoidAsync("console.log", + $"[SignedPage] Cache miss or no sid. sid={Sid}"); + } + } + + public void Dispose() + { + _report?.Dispose(); + } + + // ----- PDF signature rendering ----- + + /// + /// Uses PdfSharp to burn the captured signature onto the PDF at each signature field. + /// Layout per field (top-left origin, Y down, units = PDF points): + /// [top 65%] signature image + /// [separator line] + /// [bottom 35%] FullName (bold) / Position (optional) / Place, Date + /// + static byte[] DrawSignaturesOnPdf( + byte[] pdfBytes, + IReadOnlyList signatures, + SignatureCaptureDto sig) + { + var imgBytes = DataUrlToBytes(sig.DataUrl); + if (imgBytes is not { Length: > 0 }) return pdfBytes; + + using var inputMs = new System.IO.MemoryStream(pdfBytes); + using var outputMs = new System.IO.MemoryStream(); + + var document = PdfSharp.Pdf.IO.PdfReader.Open( + inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify); + + const double sigW = 1.77 * 72; // 127.44 pt + const double sigH = 1.96 * 72; // 141.12 pt + const double imgRatio = 0.52; // top 52% = image + const double lineH = 11.5; // fixed row height matching font size (bold 7.5pt + normal 6.5pt) + const double bgPad = 3.0; // background box padding around content (pt) + + var black = PdfSharp.Drawing.XColor.FromArgb(255, 20, 20, 20); + var darkGray = PdfSharp.Drawing.XColor.FromArgb(255, 80, 80, 80); + var lineColor = PdfSharp.Drawing.XColor.FromArgb(180, 100, 100, 120); + + var bgColor = PdfSharp.Drawing.XColor.FromArgb(255, 255, 253, 240); + var bgBrush = new PdfSharp.Drawing.XSolidBrush(bgColor); + + var fontBold = new PdfSharp.Drawing.XFont("Arial", 7.5, PdfSharp.Drawing.XFontStyleEx.Bold); + var fontNormal = new PdfSharp.Drawing.XFont("Arial", 6.5, PdfSharp.Drawing.XFontStyleEx.Regular); + var linePen = new PdfSharp.Drawing.XPen(lineColor, 0.5); + + var fmtLeft = new PdfSharp.Drawing.XStringFormat + { + Alignment = PdfSharp.Drawing.XStringAlignment.Near, + LineAlignment = PdfSharp.Drawing.XLineAlignment.Near, + }; + + var date = DateTime.Now.ToString("dd.MM.yyyy"); + + foreach (var field in signatures) + { + int pageIndex = field.Page - 1; + if (pageIndex < 0 || pageIndex >= document.PageCount) continue; + + var page = document.Pages[pageIndex]; + + using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page); + + double x = field.X; + double y = field.Y; + + // --- Calculate layout positions first (needed for bg rect) --- + double imgH = sigH * imgRatio; + double lineY = y + imgH + 1.0; // 1pt gap between image and separator + double textY = lineY + 1.5; // 1.5pt gap below separator line + double padding = 3; + + // Row 1: FullName + double row1Y = textY; + // Row 2: Position (optional) + double row2Y = row1Y + lineH; + // Row 3: Place, Date — immediately after row2 regardless of position + double row3Y = !string.IsNullOrWhiteSpace(sig.Position) ? row2Y + lineH : row2Y; + double contentBottom = row3Y + lineH; + + // --- Background rectangle sized to actual content (not full sigH) --- + var bgRect = new PdfSharp.Drawing.XRect( + x - bgPad, + y - bgPad, + sigW + bgPad * 2, + (contentBottom - y) + bgPad * 2); + gfx.DrawRectangle(bgBrush, bgRect); + + // --- Image area --- + var imgRect = new PdfSharp.Drawing.XRect(x, y, sigW, imgH); + using var imgStream = new System.IO.MemoryStream(imgBytes); + var xImg = PdfSharp.Drawing.XImage.FromStream(imgStream); + gfx.DrawImage(xImg, imgRect); + + // --- Separator line --- + gfx.DrawLine(linePen, x + 2, lineY, x + sigW - 2, lineY); + + // --- Text rows --- + // Row 1: FullName (bold) + var nameRect = new PdfSharp.Drawing.XRect(x + padding, row1Y, sigW - padding * 2, lineH); + gfx.DrawString(sig.FullName, fontBold, new PdfSharp.Drawing.XSolidBrush(black), nameRect, fmtLeft); + + // Row 2: Position (optional) + if (!string.IsNullOrWhiteSpace(sig.Position)) + { + var posRect = new PdfSharp.Drawing.XRect(x + padding, row2Y, sigW - padding * 2, lineH); + gfx.DrawString(sig.Position, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), posRect, fmtLeft); + } + + // Row 3: Place, Date + var placeDate = $"{sig.Place}, {date}"; + var dateRect = new PdfSharp.Drawing.XRect(x + padding, row3Y, sigW - padding * 2, lineH); + gfx.DrawString(placeDate, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), dateRect, fmtLeft); + } + + document.Save(outputMs); + return outputMs.ToArray(); + } + + static byte[]? DataUrlToBytes(string? dataUrl) + { + if (string.IsNullOrWhiteSpace(dataUrl)) return null; + var comma = dataUrl.IndexOf(','); + if (comma < 0) return null; + return Convert.FromBase64String(dataUrl[(comma + 1)..]); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage.razor new file mode 100644 index 00000000..d58d6c3e --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage.razor @@ -0,0 +1,1207 @@ +@page "/envelope/{EnvelopeKey}/deprc" +@rendermode InteractiveServer +@using EnvelopeGenerator.Server.Client.Models +@using EnvelopeGenerator.Server.Client.Models.Constants +@using EnvelopeGenerator.Server.Client.Services +@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver +@using System.Security.Claims +@using Microsoft.Extensions.Options +@using EnvelopeGenerator.Server.Client.Options +@using Microsoft.JSInterop +@using DevExpress.Blazor +@inject NavigationManager Navigation +@inject IOptions PdfViewerOptions +@inject IJSRuntime JSRuntime +@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService +@inject AppVersionService AppVersion +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService +@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService +@inject ILogger logger +@implements IAsyncDisposable + + + + + + + + +
+
+
+ @* Row 1: Title + Sender + Badges + Logout *@ +
+ @* Left: Title + Sender *@ +
+ @if (_envelopeReceiver is not null) + { +
+ @(_envelopeReceiver.Envelope?.Title ?? "Dokument") +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) + { + + Von + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) + { + @_envelopeReceiver.Envelope.User.FullName + } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) + { + <@_envelopeReceiver.Envelope.User.Email> + } + @if (_envelopeReceiver.Envelope?.AddedWhen != null) + { +  · @_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy") + } + + } + } + else + { +
Dokumentenansicht
+ } +
+ + @* Right: Badges + Logout *@ +
+ @if (_envelopeReceiver is not null) + { +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) + { + + + + + @_envelopeReceiver.Name + + } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) + { + + Von @_envelopeReceiver.Envelope.User.FullName + + } + @{ + int sigCount = _signatures.Count; + } + @if (sigCount > 0) + { + + + + + @sigCount + + } + @if (_envelopeReceiver.Envelope?.UseAccessCode ?? false) + { + + + + + Code + + } + @if (_envelopeReceiver.Envelope?.TFAEnabled ?? false) + { + + + + + + 2FA + + } +
+ + } + + @* Logout button *@ + @if (!string.IsNullOrWhiteSpace(EnvelopeKey)) + { + + } +
+
+ + @* Row 2: Messages (visible text) *@ + @if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))) + { +
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) + { +
+ 📧 + @_envelopeReceiver.Envelope.Message +
+ } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) + { +
+ 🔒 + @_envelopeReceiver.PrivateMessage +
+ } +
+ } +
+
+ +
+ @if (_isLoading) + { +
+
+
+ L�dt... +
+

Dokument wird geladen...

+
+
+ } + else if (_errorMessage is not null) + { +
+
+
+ + + + +
+
Fehler beim Laden des Dokuments
+

@_errorMessage

+
+
+
+
+ } + else if (!string.IsNullOrWhiteSpace(_pdfDataUrl)) + { +
+ @if (_pdfLoaded) + { +
+
+ +
+ +
+ +
+ +
+ + / @_totalPages +
+ +
+ +
+ +
+ +
+ +
@(_currentZoom)%
+
+ +
+ +
+ + @if (_totalSignatures > 0) + { +
+ +
+ +
+ +
+ + +
+ + + + + @if (_currentSignatureIndex > 0) + { + #@_currentSignatureIndex + | + } + @_signedSignatures +  /  + @_totalSignatures + + @if (_unsignedSignatures > 0) + { + @_unsignedSignatures offen + } + else + { + ✓ Komplett + } +
+ + +
+ +
+ + @* Reset button - only show when signatures are signed *@ + @if (_signedSignatures > 0) + { +
+ +
+ } + } +
+ } +
+ @if (_pdfLoaded && _showThumbnails) + { + +
+
+ @for (int i = 1; i <= _totalPages; i++) + { + var pageNum = i; +
+
+ +
+
@pageNum
+
+ } +
+
+ +
+
+ } +
+
+ +
+
+
+
+
+
+ } + else + { +
+
+
+ + + + Dokument konnte nicht geladen werden. +
+
+
+ } +
+
+ + + + + + @if (_activeSignatureTab == SignatureTabDraw) + { +

Bitte unterschreiben Sie im folgenden Feld.

+ + } + else if (_activeSignatureTab == SignatureTabText) + { +

Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.

+
+
+ +
+
+ +
+
+ + } + else + { +

Laden Sie ein Bild Ihrer Unterschrift hoch.

+ + + } + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + @if (!string.IsNullOrWhiteSpace(_popupValidationMessage)) + { +
+ @_popupValidationMessage +
+ } +
+ +
+ + +
+
+
+ +@code { + // Signature tab constants + const string SignatureTabDraw = "draw"; + const string SignatureTabText = "text"; + const string SignatureTabImage = "image"; + const string DrawCanvasId = "envelope-signature-pad"; + const string TypedCanvasId = "envelope-typed-signature-pad"; + const string ImageInputId = "envelope-signature-image-input"; + const string ImageCanvasId = "envelope-image-signature-pad"; + + readonly (string Text, string Value)[] TypedSignatureFonts = { + ("Brush Script", "'Brush Script MT', cursive"), + ("Segoe Script", "'Segoe Script', cursive"), + ("Lucida Handwriting", "'Lucida Handwriting', cursive"), + ("Comic Sans", "'Comic Sans MS', cursive"), + ("Cursive", "cursive") +}; + + [Parameter] public string? EnvelopeKey { get; set; } + + bool _isLoading = true; + string? _errorMessage; + string? _pdfDataUrl; + bool _pdfLoaded = false; + int _currentPage = 1; + int _totalPages = 0; + int _currentZoom = 150; + bool _showThumbnails = true; + bool _isLoggingOut = false; + DotNetObjectReference? _dotNetRef; + IReadOnlyList _signatures = []; + EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; + ClaimsPrincipal? _receiverUser; + + // Signature navigation state + int _totalSignatures = 0; + int _signedSignatures = 0; + int _unsignedSignatures = 0; + int _currentSignatureIndex = 0; // Current signature index (1-based) + + // Signature state + SignatureCaptureDto? _capturedSignature; + bool _signaturePopupVisible = false; + string? _popupValidationMessage; + string _activeSignatureTab = SignatureTabDraw; + string _typedSignatureText = string.Empty; + string _typedSignatureFont = "'Brush Script MT', cursive"; + string _signerFullName = string.Empty; + string _signerPosition = string.Empty; + string _signaturePlace = string.Empty; + + // Resizable splitter state + int _thumbnailWidth = 260; + bool _isResizing = false; + int _resizeStartX = 0; + int _resizeStartWidth = 0; + const int MinThumbnailWidth = 150; + const int MaxThumbnailWidth = 400; + + async Task LogoutAsync() + { + if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return; + _isLoggingOut = true; + await InvokeAsync(StateHasChanged); + await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey); + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); + } + + protected override async Task OnInitializedAsync() + { + if (string.IsNullOrWhiteSpace(EnvelopeKey)) + { + _errorMessage = "Envelope-Schlüssel fehlt."; + _isLoading = false; + return; + } + + _receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey); + if (_receiverUser is null) + { + Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } + + try + { + var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser); + + if (pdfBytes is { Length: > 0 }) + { + var base64 = Convert.ToBase64String(pdfBytes); + _pdfDataUrl = $"data:application/pdf;base64,{base64}"; + } + else + { + _errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen."; + } + + _signatures = await PageDataService.GetSignaturesAsync(_receiverUser); + + _envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey); + if (_envelopeReceiver is null) + { + logger.LogWarning("Envelope receiver data is null for envelope {EnvelopeKey}", EnvelopeKey); + } + + logger.LogInformation("Loaded {SignatureCount} signatures for envelope {EnvelopeKey}", _signatures.Count, EnvelopeKey); + + // Try to load cached signature first + try + { + var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser); + if (cachedSignature is not null) + { + _capturedSignature = cachedSignature; + _signerFullName = cachedSignature.FullName; + _signerPosition = cachedSignature.Position; + _signaturePlace = cachedSignature.Place; + _signaturePopupVisible = false; + + logger.LogInformation("Cached signature loaded for envelope {EnvelopeKey}", EnvelopeKey); + } + else + { + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + _popupValidationMessage = null; + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to load cached signature, showing popup"); + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + _popupValidationMessage = null; + } + + } + catch (HttpRequestException ex) + { + _errorMessage = $"Dokument konnte nicht geladen werden: {ex.Message}"; + logger.LogError(ex, "Failed to load document for envelope {EnvelopeKey}", EnvelopeKey); + } + catch (Exception ex) + { + _errorMessage = $"Fehler: {ex.Message}"; + logger.LogError(ex, "Unexpected error during initialization for envelope {EnvelopeKey}", EnvelopeKey); + } + + _isLoading = false; + await InvokeAsync(StateHasChanged); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Load saved thumbnail width from localStorage + try + { + var savedWidth = await JSRuntime.InvokeAsync("localStorage.getItem", "envelopeViewer_thumbnailWidth"); + if (!string.IsNullOrEmpty(savedWidth) && int.TryParse(savedWidth, out var width)) + { + _thumbnailWidth = Math.Clamp(width, MinThumbnailWidth, MaxThumbnailWidth); + await InvokeAsync(StateHasChanged); + } + } + catch + { + // Ignore localStorage errors + } + } + + if (!_pdfLoaded && !string.IsNullOrWhiteSpace(_pdfDataUrl)) + { + await Task.Delay(500); + + try + { + _dotNetRef = DotNetObjectReference.Create(this); + + // Send quality options to JavaScript + var options = PdfViewerOptions.Value; + await JSRuntime.InvokeVoidAsync("pdfViewer.setQualityOptions", new + { + options.ThumbnailBaseScale, + options.ThumbnailEnableHiDPI, + options.ThumbnailMaxDPR, + options.MainCanvasEnableHiDPI, + options.MainCanvasMaxDPR, + options.EnableSmoothZoom, + options.ZoomTransitionDuration, + options.RenderingOpacity, + options.ZoomStepPercentage + }); + + var success = await JSRuntime.InvokeAsync("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef); + + if (success) + { + _pdfLoaded = true; + _totalPages = await JSRuntime.InvokeAsync("pdfViewer.getTotalPages"); + _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + + // Attach resize listeners + await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef); + + + await InvokeAsync(StateHasChanged); + + // Wait for DOM to be ready, then render thumbnails + await Task.Delay(100); + await RenderThumbnailsAsync(); + + // Render signature buttons + await RenderSignatureButtonsAsync(); + } + } + catch (Exception ex) + { + _errorMessage = $"PDF.js Fehler: {ex.Message}"; + await InvokeAsync(StateHasChanged); + } + } + } + + [JSInvokable] + public async Task OnZoomChanged(double scale) + { + _currentZoom = (int)(scale * 100); + await InvokeAsync(StateHasChanged); + + // Small delay for canvas render to complete (reduced from 100ms to 10ms) + await Task.Delay(10); + await RenderSignatureButtonsAsync(); + } + + async Task NextPage() + { + if (await JSRuntime.InvokeAsync("pdfViewer.nextPage")) + { + _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + await RenderSignatureButtonsAsync(); + } + } + + async Task PreviousPage() + { + if (await JSRuntime.InvokeAsync("pdfViewer.previousPage")) + { + _currentPage = await JSRuntime.InvokeAsync("pdfViewer.getCurrentPage"); + await RenderSignatureButtonsAsync(); + } + } + + async Task ZoomIn() + { + if (_currentZoom >= 300) return; + await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn"); + var scale = await JSRuntime.InvokeAsync("pdfViewer.getScale"); + _currentZoom = (int)(scale * 100); + + // Update signature overlay positions after zoom + await RenderSignatureButtonsAsync(); + } + + async Task ZoomOut() + { + if (_currentZoom <= 50) return; + await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut"); + var scale = await JSRuntime.InvokeAsync("pdfViewer.getScale"); + _currentZoom = (int)(scale * 100); + + // Update signature overlay positions after zoom + await RenderSignatureButtonsAsync(); + } + + async Task SetZoom(int percentage) + { + var scale = percentage / 100.0; + await JSRuntime.InvokeVoidAsync("pdfViewer.setScale", scale); + _currentZoom = percentage; + } + + async Task OnZoomSliderChanged(ChangeEventArgs e) + { + if (int.TryParse(e.Value?.ToString(), out var zoom)) + { + await SetZoom(zoom); + + // Update signature overlay positions after zoom + await RenderSignatureButtonsAsync(); + } + } + + async Task OnPageInputChanged(ChangeEventArgs e) + { + if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages) + { + if (await JSRuntime.InvokeAsync("pdfViewer.goToPage", pageNum)) + { + _currentPage = pageNum; + } + } + } + + async Task FitToWidth() + { + await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth"); + var scale = await JSRuntime.InvokeAsync("pdfViewer.getScale"); + _currentZoom = (int)(scale * 100); + } + + async Task ToggleThumbnails() + { + _showThumbnails = !_showThumbnails; + + // Re-render thumbnails when showing them + if (_showThumbnails && _pdfLoaded) + { + await InvokeAsync(StateHasChanged); // Force UI update first + await Task.Delay(150); // Wait for DOM to render canvas elements + await RenderThumbnailsAsync(); + } + } + + async Task GoToPageFromThumbnail(int pageNum) + { + if (await JSRuntime.InvokeAsync("pdfViewer.goToPage", pageNum)) + { + _currentPage = pageNum; + await RenderSignatureButtonsAsync(); + } + } + + async Task RenderSignatureButtonsAsync() + { + if (_signatures.Count == 0 || !_pdfLoaded) return; + + try + { + await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef); + await UpdateSignatureCounterAsync(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Signature button rendering error: {ex.Message}"); + } + } + + [JSInvokable] + public async Task OnSignatureButtonClick(int signatureId) + { + if (_capturedSignature == null) + { + // No signature captured yet - should not happen as popup is shown on page load + return; + } + + // Apply signature to PDF canvas + await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature", + signatureId, + _capturedSignature.DataUrl, + _capturedSignature.FullName, + _capturedSignature.Position, + _capturedSignature.Place); + + // Update counter + await UpdateSignatureCounterAsync(); + } + + [JSInvokable] + public async Task OnSignatureNavChanged() + { + await UpdateSignatureCounterAsync(); + } + + [JSInvokable] + public async Task OnPageChangedBySignatureNav(int newPage) + { + _currentPage = newPage; + await RenderSignatureButtonsAsync(); + } + + async Task UpdateSignatureCounterAsync() + { + try + { + var state = await JSRuntime.InvokeAsync("pdfViewer.getSignatureNavState"); + _totalSignatures = state.Total; + _signedSignatures = state.Signed; + _unsignedSignatures = state.Unsigned; + _currentSignatureIndex = state.CurrentIndex; // Current signature + await InvokeAsync(StateHasChanged); + } + catch + { + // Ignore errors during counter update + } + } + + async Task GoToPreviousSignature() + { + await JSRuntime.InvokeVoidAsync("pdfViewer.goToPreviousSignature", _dotNetRef); + } + + async Task GoToNextSignature() + { + await JSRuntime.InvokeVoidAsync("pdfViewer.goToNextSignature", _dotNetRef); + } + + void RestartSigning() + { + // Force page reload to reset all signatures and state + Navigation.NavigateTo(Navigation.Uri, forceLoad: true); + } + + record SignatureNavState(int Total, int Signed, int Unsigned, int CurrentIndex, bool CanGoPrev, bool CanGoNext); + + string GetSignatureButtonTitle() + { + if (_signedSignatures > 0) + return "Unterschrift ist gesperrt – bitte Seite neu laden, um zu ändern"; + + return _capturedSignature is not null + ? "Unterschrift ändern" + : "Unterschrift erstellen"; + } + + void HandleSignatureChangeClick() + { + // If any signature is applied, button is disabled - this won't be called + // But just in case, do nothing + if (_signedSignatures > 0) + return; + + // No signatures applied - open popup normally + OpenSignaturePopup(); + } + + // Signature popup methods + void OpenSignaturePopup() + { + // Open popup with current signature (edit mode) + _activeSignatureTab = SignatureTabDraw; + _signaturePopupVisible = true; + _popupValidationMessage = null; + + // Load current signature info into form fields + if (_capturedSignature is not null) + { + _signerFullName = _capturedSignature.FullName; + _signerPosition = _capturedSignature.Position; + _signaturePlace = _capturedSignature.Place; + } + } + + async Task OnPopupShownAsync() + { + await InitializeActiveSignatureTabAsync(); + + // If there's an existing signature and we're on draw tab, load it to canvas + if (_capturedSignature is not null && _activeSignatureTab == SignatureTabDraw) + { + await Task.Delay(100); // Wait for canvas to be ready + await JSRuntime.InvokeVoidAsync("receiverSignature.loadExistingSignature", DrawCanvasId, _capturedSignature.DataUrl); + } + } + + async Task SetSignatureTabAsync(string tab) + { + _activeSignatureTab = tab; + _popupValidationMessage = null; + await InvokeAsync(StateHasChanged); + await Task.Delay(50); + await InitializeActiveSignatureTabAsync(); + } + + async Task InitializeActiveSignatureTabAsync() + { + if (_activeSignatureTab == SignatureTabDraw) + { + await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId); + } + else if (_activeSignatureTab == SignatureTabText) + { + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId); + await RenderTypedSignatureAsync(); + } + else + { + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId); + } + } + + async Task RenewSignatureAsync() + { + _popupValidationMessage = null; + + if (_activeSignatureTab == SignatureTabDraw) + { + await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId); + } + else if (_activeSignatureTab == SignatureTabText) + { + _typedSignatureText = string.Empty; + await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId); + } + else + { + await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId); + } + } + + async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) + { + _typedSignatureText = args.Value?.ToString() ?? string.Empty; + await RenderTypedSignatureAsync(); + } + + async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) + { + _typedSignatureFont = args.Value?.ToString() ?? _typedSignatureFont; + await RenderTypedSignatureAsync(); + } + + async Task RenderTypedSignatureAsync() + { + await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, _typedSignatureText, _typedSignatureFont); + } + + async Task SaveSignatureAsync() + { + if (string.IsNullOrWhiteSpace(_signerFullName)) + { + _popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein."; + return; + } + if (string.IsNullOrWhiteSpace(_signaturePlace)) + { + _popupValidationMessage = "Bitte geben Sie den Ort ein."; + return; + } + var signatureDataUrl = await GetActiveSignatureDataUrlAsync(); + if (string.IsNullOrWhiteSpace(signatureDataUrl)) + { + _popupValidationMessage = "Die Unterschrift ist erforderlich."; + return; + } + + _popupValidationMessage = null; + _capturedSignature = new SignatureCaptureDto + { + DataUrl = signatureDataUrl, + FullName = _signerFullName.Trim(), + Position = _signerPosition.Trim(), + Place = _signaturePlace.Trim() + }; + _signaturePopupVisible = false; + + // Save to cache (fire-and-forget, ignore errors) + if (!string.IsNullOrWhiteSpace(EnvelopeKey)) + { + _ = Task.Run(async () => + { + try + { + if (_receiverUser is not null) + { + await PageDataService.SaveCachedSignatureAsync(_receiverUser, _capturedSignature); + } + } + catch + { + // Ignore cache errors + } + }); + } + + await InvokeAsync(StateHasChanged); + Console.WriteLine($"Signature saved: {_signerFullName}, {_signaturePlace}"); + } + + async Task GetActiveSignatureDataUrlAsync() + { + if (_activeSignatureTab == SignatureTabDraw) + return await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", DrawCanvasId); + + if (_activeSignatureTab == SignatureTabText) + { + await RenderTypedSignatureAsync(); + return await JSRuntime.InvokeAsync("receiverSignature.getTypedDataUrl", TypedCanvasId); + } + + return await JSRuntime.InvokeAsync("receiverSignature.getImageDataUrl", ImageCanvasId); + } + + async Task RenderThumbnailsAsync() + { + try + { + var delay = PdfViewerOptions.Value.ThumbnailRenderDelay; + + // Sequential rendering to avoid overwhelming the browser + for (int i = 1; i <= _totalPages; i++) + { + await JSRuntime.InvokeVoidAsync("pdfViewer.renderThumbnail", i, $"thumb-canvas-{i}"); + + // Configurable delay between renders + if (i < _totalPages) + { + await Task.Delay(delay); + } + } + } + catch (Exception ex) + { + // Thumbnail rendering is not critical + System.Diagnostics.Debug.WriteLine($"Thumbnail rendering error: {ex.Message}"); + } + } + + // Resizable splitter methods + void OnSplitterMouseDown(MouseEventArgs e) + { + _isResizing = true; + _resizeStartX = (int)e.ClientX; + _resizeStartWidth = _thumbnailWidth; + + // Add resizing class to body to prevent text selection + _ = JSRuntime.InvokeVoidAsync("eval", "document.body.classList.add('resizing')"); + _ = JSRuntime.InvokeVoidAsync("pdfViewer.startResize"); + } + + [JSInvokable] + public async Task OnSplitterMouseMove(int clientX) + { + if (!_isResizing) return; + + var delta = clientX - _resizeStartX; + var newWidth = _resizeStartWidth + delta; + + // Clamp to min/max + _thumbnailWidth = Math.Clamp(newWidth, MinThumbnailWidth, MaxThumbnailWidth); + + await InvokeAsync(StateHasChanged); + } + + [JSInvokable] + public async Task OnSplitterMouseUp() + { + if (!_isResizing) return; + + _isResizing = false; + + // Remove resizing class from body + await JSRuntime.InvokeVoidAsync("eval", "document.body.classList.remove('resizing')"); + + // Save preference to localStorage + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "envelopeViewer_thumbnailWidth", _thumbnailWidth.ToString()); + + await InvokeAsync(StateHasChanged); + } + + public async ValueTask DisposeAsync() + { + if (_pdfLoaded) + { + try + { + await JSRuntime.InvokeVoidAsync("pdfViewer.dispose"); + } + catch + { + // Ignore errors during disposal + } + } + _dotNetRef?.Dispose(); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_DxPdfViewer.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_DxPdfViewer.razor new file mode 100644 index 00000000..a9a3d66f --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_DxPdfViewer.razor @@ -0,0 +1,114 @@ +@page "/envelope/DxPdfViewer" +@rendermode InteractiveServer +@using System.IO +@using DevExpress.Blazor +@using System.Reflection +@using DevExpress.Blazor.PdfViewer + + + + + +
+ + Drag and Drop File Hereor + +
+ + + +@if (DocumentContent != null && DocumentContent.Length > 0) +{ +
+ PDF loaded: @DocumentContent.Length bytes +
+ +} +else +{ +
+ Please upload a PDF file to view it. +
+} + +@code { + readonly List ALLOWED_FILE_TYPES = new List { ".pdf" }; + DxFileInput fileInput; + byte[] DocumentContent { get; set; } + protected override void OnInitialized() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.Server.Resources.Invoice.pdf"); + if (stream != null) + { + using (stream) + using (var binaryReader = new BinaryReader(stream)) + DocumentContent = binaryReader.ReadBytes((int)stream.Length); + } + } + protected async Task OnFilesUploading(FilesUploadingEventArgs args) + { + using (MemoryStream stream = new MemoryStream()) + { + IFileInputSelectedFile file = args.Files[0]; + await file.OpenReadStream(file.Size).CopyToAsync(stream); + DocumentContent = stream.ToArray(); + await InvokeAsync(StateHasChanged); + } + } +} + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_DxReportViewer.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_DxReportViewer.razor new file mode 100644 index 00000000..1eb5d9ab --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_DxReportViewer.razor @@ -0,0 +1,50 @@ +@page "/envelope/{EnvelopeKey}/DxReportViewer" +@rendermode InteractiveServer +@using XtraReport = DevExpress.XtraReports.UI.XtraReport +@using DevExpress.Blazor.Reporting +@using Microsoft.Extensions.Options +@using EnvelopeGenerator.Server.Client.Options +@using EnvelopeGenerator.Server.Client.Services +@inject InMemoryReportStorageWebExtension ReportStorage +@inject DocumentService DocumentService +@inject IOptions AppOptions + + + + + +@if (_report is not null) { + +} + +@code { + [Parameter] public string EnvelopeKey { get; init; } = null!; + + XtraReport? _report = null; + + protected override async Task OnInitializedAsync() + { + _report = await CreateReport(); + } + + async Task CreateReport() + { + if (AppOptions.Value.UsePredefinedReports) + { + return Client.PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport"); + } + else + { + + var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey); + if (pdfBytes is null || pdfBytes.Length == 0) + throw new InvalidOperationException($"No PDF bytes found for EnvelopeKey: {EnvelopeKey}"); + + var report = new XtraReport(); + var detail = new DevExpress.XtraReports.UI.DetailBand(); + report.Bands.Add(detail); + detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = pdfBytes, GenerateOwnPages = true }); + return report; + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_embed.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_embed.razor new file mode 100644 index 00000000..e5d61ee3 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/Test/EnvelopeReceiverPage_embed.razor @@ -0,0 +1,124 @@ +@page "/envelope/Embed" +@rendermode InteractiveServer +@using System.IO +@using DevExpress.Blazor +@using System.Reflection + + + + + +
+ + Drag and Drop File Hereor + +
+ + + +@if (DocumentContent != null && DocumentContent.Length > 0) +{ +
+ PDF loaded: @DocumentContent.Length bytes +
+ +} +else +{ +
+ Please upload a PDF file to view it. +
+} + +@code { + readonly List ALLOWED_FILE_TYPES = new List { ".pdf" }; + DxFileInput fileInput; + byte[] DocumentContent { get; set; } + + protected override void OnInitialized() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.Server.Resources.Invoice.pdf"); + if (stream != null) + { + using (stream) + using (var binaryReader = new BinaryReader(stream)) + DocumentContent = binaryReader.ReadBytes((int)stream.Length); + } + } + + protected async Task OnFilesUploading(FilesUploadingEventArgs args) + { + using (MemoryStream stream = new MemoryStream()) + { + IFileInputSelectedFile file = args.Files[0]; + await file.OpenReadStream(file.Size).CopyToAsync(stream); + DocumentContent = stream.ToArray(); + await InvokeAsync(StateHasChanged); + } + } + + private string GetPdfDataUrl() + { + if (DocumentContent == null || DocumentContent.Length == 0) + return string.Empty; + + string base64 = Convert.ToBase64String(DocumentContent); + return $"data:application/pdf;base64,{base64}#toolbar=0&navpanes=0&scrollbar=1"; + } +} + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/_Imports.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/_Imports.razor new file mode 100644 index 00000000..f494bffa --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using EnvelopeGenerator.Server +@using EnvelopeGenerator.Server.Client +@using EnvelopeGenerator.Server.Components +@using DevExpress.Blazor diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AnnotationController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AnnotationController.cs new file mode 100644 index 00000000..4847945b --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AnnotationController.cs @@ -0,0 +1,132 @@ +using DigitalData.Core.Abstraction.Application.DTO; +using DigitalData.Core.Exceptions; +using EnvelopeGenerator.Server.Extensions; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Extensions; +using EnvelopeGenerator.Application.Common.Interfaces.Services; +using EnvelopeGenerator.Application.Common.Notifications.DocSigned; +using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature; +using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; +using EnvelopeGenerator.Application.Histories.Queries; +using EnvelopeGenerator.Domain.Constants; +using MediatR; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Manages annotations and signature lifecycle for envelopes. +/// +[Authorize(Policy = AuthPolicy.Receiver)] +[ApiController] +[Route("api/[controller]")] +public class AnnotationController : ControllerBase +{ + [Obsolete("Use MediatR")] + private readonly IEnvelopeHistoryService _historyService; + + [Obsolete("Use MediatR")] + private readonly IEnvelopeReceiverService _envelopeReceiverService; + + private readonly IMediator _mediator; + + private readonly ILogger _logger; + + /// + /// Initializes a new instance of . + /// + [Obsolete("Use MediatR")] + public AnnotationController( + ILogger logger, + IEnvelopeHistoryService envelopeHistoryService, + IEnvelopeReceiverService envelopeReceiverService, + IMediator mediator) + { + _historyService = envelopeHistoryService; + _envelopeReceiverService = envelopeReceiverService; + _mediator = mediator; + _logger = logger; + } + + /// + /// Creates or updates annotations for the authenticated envelope receiver. + /// + /// Annotation payload. + /// Cancellation token. + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpPost] + [Obsolete("PSPDF Kit will no longer be used.")] + public async Task CreateOrUpdate([FromBody] PsPdfKitAnnotation? psPdfKitAnnotation = null, CancellationToken cancel = default) + { + var signature = User.ReceiverSignature(); + var uuid = User.EnvelopeUuid(); + + var envelopeReceiver = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel).ThrowIfNull(Exceptions.NotFound); + + if (!envelopeReceiver.Envelope!.ReadOnly && psPdfKitAnnotation is null) + return BadRequest(); + + if (await _mediator.IsSignedAsync(uuid, signature, cancel)) + return Problem(statusCode: StatusCodes.Status409Conflict); + else if (await _mediator.AnyHistoryAsync(uuid, new[] { EnvelopeStatus.EnvelopeRejected, EnvelopeStatus.DocumentRejected }, cancel)) + return Problem(statusCode: StatusCodes.Status423Locked); + + var envelopeReceiverDto = await _mediator.ReadEnvelopeReceiverAsync(uuid, signature, cancel); + var docSignedNotification = envelopeReceiverDto is not null + ? new DocSignedNotification { EnvelopeReceiver = envelopeReceiverDto, PsPdfKitAnnotation = psPdfKitAnnotation } + : throw new NotFoundException("Envelope receiver is not found."); + + try + { + await _mediator.Publish(docSignedNotification, cancel); + } + catch (Exception) + { + await _mediator.Publish(new RemoveSignatureNotification() + { + EnvelopeId = docSignedNotification.EnvelopeReceiver.EnvelopeId, + ReceiverId = docSignedNotification.EnvelopeReceiver.ReceiverId + }, cancel); + throw; + } + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + return Ok(); + } + + /// + /// Rejects the document for the current receiver. + /// + /// Optional rejection reason. + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpPost("reject")] + [Obsolete("Use MediatR")] + public async Task Reject([FromBody] string? reason = null) + { + var signature = User.ReceiverSignature(); + var uuid = User.EnvelopeUuid(); + var mail = User.ReceiverMail(); + + var envRcvRes = await _envelopeReceiverService.ReadByUuidSignatureAsync(uuid: uuid, signature: signature); + + if (envRcvRes.IsFailed) + { + _logger.LogNotice(envRcvRes.Notices); + return Unauthorized("you are not authorized"); + } + + var histRes = await _historyService.RecordAsync(envRcvRes.Data.EnvelopeId, userReference: mail, EnvelopeStatus.DocumentRejected, comment: reason); + if (histRes.IsSuccess) + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return NoContent(); + } + + _logger.LogEnvelopeError(uuid: uuid, signature: signature, message: "Unexpected error happened in api/envelope/reject"); + _logger.LogNotice(histRes.Notices); + return StatusCode(500, histRes.Messages); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs new file mode 100644 index 00000000..314f4d90 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs @@ -0,0 +1,111 @@ +using DigitalData.Auth.Claims; +using EnvelopeGenerator.Server.Controllers.Interfaces; +using EnvelopeGenerator.Server.Models; +using EnvelopeGenerator.Domain.Constants; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus. +/// +[Route("api/[controller]")] +[ApiController] +public partial class AuthController(IOptions authTokenKeyOptions, IAuthorizationService authService) : ControllerBase, IAuthController +{ + private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value; + + /// + /// + /// + public IAuthorizationService AuthService { get; } = authService; + + /// + /// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie) + /// + /// + /// Gibt eine HTTP 200 oder 401. + /// + /// + /// Sample request: + /// + /// POST /api/auth/logout + /// + /// + /// Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat. + /// Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben. + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpPost("logout")] + public IActionResult Logout() + { + Response.Cookies.Delete(authTokenKeys.Cookie); + return Ok(); + } + + /// + /// Prüft, ob der Benutzer ein autorisiertes Token hat. + /// + /// Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401 + /// + /// Sample request: + /// + /// GET /api/auth + /// + /// + /// Wenn es einen autorisierten Cookie gibt. + /// Wenn kein Cookie vorhanden ist oder nicht autorisierte. + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [HttpGet("check")] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + public IActionResult Check(string? role = null) + => role is not null && !User.IsInRole(role) + ? Unauthorized() + : Ok(); + + /// + /// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key. + /// The request must carry a cookie named AuthTokenSignFLOWReceiver.{envelopeKey}. + /// + /// The unique envelope key extracted from the route. + /// Valid per-envelope token found. + /// Token is missing, expired or invalid. + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpGet("check/envelope/{envelopeKey}")] + public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok(); + + /// + /// Removes the per-envelope receiver cookie for the given envelope key. + /// + /// The unique envelope key whose cookie should be deleted. + /// Cookie successfully deleted. + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [HttpPost("logout/envelope/{envelopeKey}")] + public IActionResult LogoutEnvelopeReceiver([FromRoute] string envelopeKey) + { + var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey); + Response.Cookies.Delete(cookieName); + return Ok(); + } + + /// + /// Removes all per-envelope receiver cookies from the current request. + /// + /// All envelope receiver cookies successfully deleted. + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [HttpPost("logout/envelope")] + public IActionResult LogoutAllEnvelopeReceivers() + { + foreach (var cookieName in Request.Cookies.Keys.Where(k => CookieNames.IsEnvelopeReceiverCookie(k, authTokenKeys.Cookie))) + Response.Cookies.Delete(cookieName); + return Ok(); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs new file mode 100644 index 00000000..0c8d0bc0 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System.Text.Json; +using EnvelopeGenerator.Server.Options; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Server.Extensions; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Manages cached data for receivers using distributed cache. +/// +[ApiController] +[Route("api/[controller]")] +[Authorize(Policy = AuthPolicy.Receiver)] +public class CacheController( + IDistributedCache cache, + IOptions cacheOptions) : ControllerBase +{ + private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:"; + + /// + /// Stores a receiver's signature in cache for the specified envelope. + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpPost("SignatureCapture/{envelopeKey}")] + public async Task SaveSignature( + [FromRoute] string envelopeKey, + [FromBody] SignatureCacheRequest request, + CancellationToken cancel) + { + var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}"; + var json = JsonSerializer.Serialize(request); + + var options = cacheOptions.Value.SignatureCacheExpiration.HasValue + ? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value } + : null; + + await cache.SetStringAsync(cacheKey, json, options ?? new DistributedCacheEntryOptions(), cancel); + + return Ok(); + } + + /// + /// Retrieves a cached signature for the specified envelope. + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpGet("SignatureCapture/{envelopeKey}")] + public async Task GetSignature([FromRoute] string envelopeKey, CancellationToken cancel) + { + var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}"; + var json = await cache.GetStringAsync(cacheKey, cancel); + + if (json is null) + return NotFound(); + + var signature = JsonSerializer.Deserialize(json); + return Ok(signature); + } + + /// + /// Deletes a cached signature for the specified envelope. + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpDelete("SignatureCapture/{envelopeKey}")] + public async Task DeleteSignature([FromRoute] string envelopeKey, CancellationToken cancel) + { + var cacheKey = $"{SignatureCacheKeyPrefix}{User.ReceiverSignature()}"; + await cache.RemoveAsync(cacheKey, cancel); + + return Ok(); + } +} + +/// +/// Request model for caching signature data. +/// +public sealed record SignatureCacheRequest( + string DataUrl, + string FullName, + string Place, + string? Position = null); \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ConfigController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ConfigController.cs new file mode 100644 index 00000000..17cf331d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ConfigController.cs @@ -0,0 +1,30 @@ +using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Exposes configuration data required by the client applications. +/// +/// +/// Initializes a new instance of . +/// +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ConfigController(IOptionsMonitor annotationParamsOptions) : ControllerBase +{ + private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue; + + /// + /// Returns annotation configuration that was previously rendered by MVC. + /// + [HttpGet("Annotations")] + [Obsolete("PSPDF Kit will no longer be used.")] + public IActionResult GetAnnotationParams() + { + return Ok(_annotationParams.AnnotationJSObject); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DocumentController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DocumentController.cs new file mode 100644 index 00000000..b1791dc0 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/DocumentController.cs @@ -0,0 +1,65 @@ +using DigitalData.Auth.Claims; +using EnvelopeGenerator.Server.Controllers.Interfaces; +using EnvelopeGenerator.Server.Extensions; +using EnvelopeGenerator.Application.Documents.Queries; +using EnvelopeGenerator.Domain.Constants; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Provides access to envelope documents for authenticated receivers. +/// +/// +/// Initializes a new instance of the class. +/// +[ApiController] +[Route("api/[controller]")] +public class DocumentController(IMediator mediator, IAuthorizationService authService, ILogger logger) : ControllerBase, IAuthController +{ + /// + /// + /// + public IAuthorizationService AuthService => authService; + + /// + /// Returns the document bytes receiver. + /// + /// Encoded envelope key. + /// Cancellation token. + [HttpGet] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + public async Task GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null) + { + if (query is null) + return BadRequest("Missing document query."); + + var senderDoc = await mediator.Send(query, cancel); + return senderDoc.ByteData is byte[] senderDocByte + ? File(senderDocByte, "application/octet-stream") + : NotFound("Document is empty."); + } + + /// + /// Gets the document for the specified envelope key. + /// + /// + /// + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpGet("{envelopeKey}")] + public async Task GetDocumentOfReceiver(string envelopeKey, CancellationToken cancel) + { + int envelopeId = User.EnvelopeId(); + + var senderDoc = await mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel); + + if (senderDoc.ByteData is not byte[] senderDocByte) + return NotFound("Document is empty."); + + Response.Headers.ContentDisposition = $"inline; filename=\"{envelopeKey}.pdf\""; + return File(senderDocByte, "application/pdf"); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EmailTemplateController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EmailTemplateController.cs new file mode 100644 index 00000000..bc58737c --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EmailTemplateController.cs @@ -0,0 +1,69 @@ +using AutoMapper; +using EnvelopeGenerator.Application.EmailTemplates.Commands; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MediatR; +using EnvelopeGenerator.Application.Common.Dto; +using DigitalData.Core.Abstraction.Application.Repository; +using EnvelopeGenerator.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Application.EmailTemplates.Queries; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Controller for managing temp templates. +/// Steuerung zur Verwaltung von E-Mail-Vorlagen. +/// +/// +/// Initialisiert eine neue Instanz der -Klasse. +/// +/// +/// Die Mediator-Instanz, die zum Senden von Befehlen und Abfragen verwendet wird. +/// +[Route("api/[controller]")] +[ApiController] +[Authorize(AuthenticationSchemes = AuthScheme.Sender)] +public class EmailTemplateController(IMediator mediator) : ControllerBase +{ + /// + /// Ruft E-Mail-Vorlagen basierend auf der angegebenen Abfrage ab. + /// Gibt alles zurück, wenn keine Id- oder Typ-Informationen eingegeben wurden. + /// + /// Die Abfrageparameter zum Abrufen von E-Mail-Vorlagen. + /// + /// Gibt HTTP-Antwort zurück + /// + /// Sample request: + /// GET /api/EmailTemplate?emailTemplateId=123 + /// + /// Wenn die E-Mail-Vorlagen erfolgreich abgerufen werden. + /// Wenn die Abfrageparameter ungültig sind. + /// Wenn der Benutzer nicht authentifiziert ist. + /// Wenn die gesuchte Abfrage nicht gefunden wird. + [HttpGet] + public async Task Get([FromQuery] ReadEmailTemplateQuery emailTemplate, CancellationToken cancel) + { + var result = await mediator.Send(emailTemplate, cancel); + return Ok(result); + } + + /// + /// Updates an temp template or resets it if no update command is provided. + /// Aktualisiert eine E-Mail-Vorlage oder setzt sie zurück, wenn kein Aktualisierungsbefehl angegeben ist. + /// + /// + /// + /// + /// Wenn die E-Mail-Vorlage erfolgreich aktualisiert oder zurückgesetzt wird. + /// Wenn die Abfrage ohne einen String gesendet wird. + /// Wenn der Benutzer nicht authentifiziert ist. + /// Wenn die gesuchte Abfrage nicht gefunden wird. + [HttpPut] + public async Task Update([FromBody] UpdateEmailTemplateCommand update, CancellationToken cancel) + { + await mediator.Send(update, cancel); + return Ok(); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs new file mode 100644 index 00000000..56e1aca2 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeController.cs @@ -0,0 +1,111 @@ +using EnvelopeGenerator.Server.Extensions; +using EnvelopeGenerator.Application.Envelopes.Commands; +using EnvelopeGenerator.Application.Envelopes.Queries; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Dieser Controller stellt Endpunkte für die Verwaltung von Umschlägen bereit. +/// +/// +/// Die API ermöglicht das Abrufen und Verwalten von Umschlägen basierend auf Benutzerinformationen und Statusfiltern. +/// +/// Mögliche Antworten: +/// - 200 OK: Die Anfrage war erfolgreich, und die angeforderten Daten werden zurückgegeben. +/// - 400 Bad Request: Die Anfrage war fehlerhaft oder unvollständig. +/// - 401 Unauthorized: Der Benutzer ist nicht authentifiziert. +/// - 403 Forbidden: Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen. +/// - 404 Not Found: Die angeforderte Ressource wurde nicht gefunden. +/// - 500 Internal Server Error: Ein unerwarteter Fehler ist aufgetreten. +/// +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class EnvelopeController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IMediator _mediator; + + /// + /// Erstellt eine neue Instanz des EnvelopeControllers. + /// + /// Der Logger, der für das Protokollieren von Informationen verwendet wird. + /// + public EnvelopeController(ILogger logger, IMediator mediator) + { + _logger = logger; + _mediator = mediator; + } + + /// + /// Ruft eine Liste von Umschlägen basierend auf dem Benutzer und den angegebenen Statusfiltern ab. + /// + /// + /// Eine IActionResult-Instanz, die die abgerufenen Umschläge oder einen Fehlerstatus enthält. + /// Die Anfrage war erfolgreich, und die Umschläge werden zurückgegeben. + /// Die Anfrage war fehlerhaft oder unvollständig. + /// Der Benutzer ist nicht authentifiziert. + /// Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen. + /// Ein unerwarteter Fehler ist aufgetreten. + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpGet] + public async Task GetAsync([FromQuery] ReadEnvelopeQuery envelope) + { + var result = await _mediator.Send(envelope.Authorize(User.GetId())); + return result.Any() ? Ok(result) : NotFound(); + } + + /// + /// Ruft das Ergebnis eines Dokuments basierend auf der ID ab. + /// + /// + /// Gibt an, ob das Dokument inline angezeigt werden soll (true) oder als Download bereitgestellt wird (false). + /// Eine IActionResult-Instanz, die das Dokument oder einen Fehlerstatus enthält. + /// Das Dokument wurde erfolgreich abgerufen. + /// Das Dokument wurde nicht gefunden oder ist nicht verfügbar. + /// Ein unerwarteter Fehler ist aufgetreten. + [HttpGet("doc-result")] + public async Task GetDocResultAsync([FromQuery] ReadEnvelopeQuery query, [FromQuery] bool view = false) + { + var envelopes = await _mediator.Send(query.Authorize(User.GetId())); + var envelope = envelopes.FirstOrDefault(); + + if (envelope is null) + return NotFound("Envelope not available."); + if (envelope.DocResult is null) + return NotFound("The document has not been fully signed or the result has not yet been released."); + + if (view) + { + Response.Headers.Append("Content-Disposition", "inline; filename=\"" + envelope.Uuid + ".pdf\""); + return File(envelope.DocResult, "application/pdf"); + } + + return File(envelope.DocResult, "application/pdf", $"{envelope.Uuid}.pdf"); + } + + /// + /// + /// + /// + /// + [NonAction] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpPost] + public async Task CreateAsync([FromBody] CreateEnvelopeCommand command) + { + var res = await _mediator.Send(command.WithAuth(User.GetId())); + + if (res is null) + { + _logger.LogError("Failed to create envelope. Envelope details: {EnvelopeDetails}", JsonConvert.SerializeObject(command)); + return StatusCode(StatusCodes.Status500InternalServerError); + } + else + return Ok(res); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs new file mode 100644 index 00000000..1ca838aa --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeReceiverController.cs @@ -0,0 +1,281 @@ +using AutoMapper; +using EnvelopeGenerator.Application.EnvelopeReceivers.Commands; +using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; +using EnvelopeGenerator.Application.Envelopes.Queries; +using EnvelopeGenerator.Domain.Entities; +using EnvelopeGenerator.Server.Models; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Options; +using System.Data; +using EnvelopeGenerator.Application.Common.SQL; +using EnvelopeGenerator.Application.Common.Dto.Receiver; +using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor; +using EnvelopeGenerator.Server.Extensions; +using EnvelopeGenerator.Domain.Constants; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Controller für die Verwaltung von Umschlagempfängern. +/// +/// +/// Dieser Controller bietet Endpunkte für das Abrufen und Verwalten von Umschlagempfängerdaten. +/// +[Route("api/[controller]")] +[Authorize] +[ApiController] +public class EnvelopeReceiverController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IMediator _mediator; + private readonly IMapper _mapper; + private readonly IEnvelopeExecutor _envelopeExecutor; + private readonly IEnvelopeReceiverExecutor _erExecutor; + private readonly IDocumentExecutor _documentExecutor; + private readonly string _cnnStr; + + /// + /// Konstruktor für den EnvelopeReceiverController. + /// + public EnvelopeReceiverController(ILogger logger, IMediator mediator, IMapper mapper, IEnvelopeExecutor envelopeExecutor, IEnvelopeReceiverExecutor erExecutor, IDocumentExecutor documentExecutor, IOptions csOpt) + { + _logger = logger; + _mediator = mediator; + _mapper = mapper; + _envelopeExecutor = envelopeExecutor; + _erExecutor = erExecutor; + _documentExecutor = documentExecutor; + _cnnStr = csOpt.Value.Value; + } + + /// + /// Ruft eine Liste von Umschlagempfängern basierend auf den angegebenen Abfrageparametern ab. + /// + /// Die Abfrageparameter für die Filterung von Umschlagempfängern. + /// Eine HTTP-Antwort mit der Liste der gefundenen Umschlagempfänger oder einem Fehlerstatus. + /// + /// Dieser Endpunkt ermöglicht es, Umschlagempfänger basierend auf dem Benutzernamen und optionalen Statusfiltern abzurufen. + /// Wenn der Benutzername nicht ermittelt werden kann, wird ein Serverfehler zurückgegeben. + /// + /// Die Liste der Umschlagempfänger wurde erfolgreich abgerufen. + /// Wenn kein autorisierter Token vorhanden ist + /// Ein unerwarteter Fehler ist aufgetreten. + [Authorize] + [HttpGet] + public async Task GetEnvelopeReceiver([FromQuery] ReadEnvelopeReceiverQuery envelopeReceiver) + { + envelopeReceiver = envelopeReceiver with { Username = User.GetUsername() }; + + var result = await _mediator.Send(envelopeReceiver); + + return Ok(result); + } + + /// + /// + /// + /// + /// + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpGet("{envelopeKey}")] + public async Task GetEnvelopeReceiverOfReceiver([FromRoute] string envelopeKey, CancellationToken cancel) + { + var er = await _mediator.Send(new ReadEnvelopeReceiverQuery() + { + Key = envelopeKey + }, cancel); + + return Ok(er.SingleOrDefault()); + } + + /// + /// Ruft den Namen des zuletzt verwendeten Empfängers basierend auf der angegebenen E-Mail-Adresse ab. + /// + /// Abfrage, bei der nur eine der Angaben ID, Signatur oder E-Mail-Adresse des Empfängers eingegeben werden muss. + /// Eine HTTP-Antwort mit dem Namen des Empfängers oder einem Fehlerstatus. + /// + /// Dieser Endpunkt ermöglicht es, den Namen des zuletzt verwendeten Empfängers basierend auf der E-Mail-Adresse abzurufen. + /// + /// Der Name des Empfängers wurde erfolgreich abgerufen. + /// Wenn kein autorisierter Token vorhanden ist + /// Kein Empfänger gefunden. + /// Ein unerwarteter Fehler ist aufgetreten. + [Authorize] + [HttpGet("salute")] + public async Task GetReceiverName([FromQuery] ReadReceiverNameQuery receiver) + { + var name = await _mediator.Send(receiver); + return name is null ? NotFound() : Ok(name); + } + + /// + /// Datenübertragungsobjekt mit Informationen zu Umschlägen, Empfängern und Unterschriften. + /// + /// + /// + /// HTTP-Antwort + /// + /// Sample request: + /// + /// POST /api/envelope + /// { + /// "title": "Vertragsdokument", + /// "message": "Bitte unterschreiben Sie dieses Dokument.", + /// "document": { + /// "dataAsBase64": "dGVzdC1iYXNlNjQtZGF0YQ==" + /// }, + /// "receivers": [ + /// { + /// "emailAddress": "example@example.com", + /// "signatures": [ + /// { + /// "x": 100, + /// "y": 200, + /// "page": 1 + /// } + /// ], + /// "name": "Max Mustermann", + /// "phoneNumber": "+49123456789" + /// } + /// ], + /// "tfaEnabled": false + /// } + /// + /// + /// Envelope-Erstellung und Sendeprozessbefehl erfolgreich + /// Wenn ein Fehler im HTTP-Body auftritt + /// Wenn kein autorisierter Token vorhanden ist + /// Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden. + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + [HttpPost] + public async Task CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel) + { + #region Create Envelope + var envelope = await _envelopeExecutor.CreateEnvelopeAsync(User.GetId(), request.Title, request.Message, request.TFAEnabled, cancel); + #endregion + + #region Add receivers + List sentReceivers = new(); + List unsentReceivers = new(); + + foreach (var receiver in request.Receivers) + { + var envelopeReceiver = await _erExecutor.AddEnvelopeReceiverAsync(envelope.Uuid, receiver.EmailAddress, receiver.Salution, receiver.PhoneNumber, cancel); + + if (envelopeReceiver is null) + unsentReceivers.Add(receiver); + else + sentReceivers.Add(envelopeReceiver); + } + + var res = _mapper.Map(envelope); + res.UnsentReceivers = unsentReceivers; + res.SentReceiver = _mapper.Map>(sentReceivers.Select(er => er.Receiver)); + #endregion + + #region Add document + var document = await _documentExecutor.CreateDocumentAsync(request.Document.DataAsBase64, envelope.Uuid, cancel); + + if (document is null) + return StatusCode(StatusCodes.Status500InternalServerError, "Document creation is failed."); + #endregion + + #region Add document element + // @DOC_ID, @RECEIVER_ID, @POSITION_X, @POSITION_Y, @PAGE + string sql = @" + DECLARE @OUT_SUCCESS bit; + + EXEC [dbo].[PRSIG_API_ADD_DOC_RECEIVER_ELEM] + {0}, + {1}, + {2}, + {3}, + {4}, + @OUT_SUCCESS OUTPUT; + + SELECT @OUT_SUCCESS as [@OUT_SUCCESS];"; + + foreach (var rcv in res.SentReceiver) + foreach (var sign in request.Receivers.Where(r => r.EmailAddress == rcv.EmailAddress).FirstOrDefault()?.DocReceiverElements ?? Enumerable.Empty()) + { + using SqlConnection conn = new(_cnnStr); + conn.Open(); + + var formattedSQL = string.Format(sql, document.Id.ToSqlParam(), rcv.Id.ToSqlParam(), sign.X.ToSqlParam(), sign.Y.ToSqlParam(), sign.Page.ToSqlParam()); + + using SqlCommand cmd = new(formattedSQL, conn); + cmd.CommandType = CommandType.Text; + + using SqlDataReader reader = cmd.ExecuteReader(); + if (reader.Read()) + { + bool outSuccess = reader.GetBoolean(0); + if (!outSuccess) + _logger.LogWarning( + "PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}", + document.Id, rcv.Id, sign.Page); + } + } + #endregion + + #region Create history + // ENV_UID, STATUS_ID, USER_ID, + string sql_hist = @" + DECLARE @OUT_SUCCESS bit; + + EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE] + {0}, + {1}, + {2}, + @OUT_SUCCESS OUTPUT; + + SELECT @OUT_SUCCESS as [@OUT_SUCCESS];"; + + using (SqlConnection conn = new(_cnnStr)) + { + conn.Open(); + var formattedSQL_hist = string.Format(sql_hist, envelope.Uuid.ToSqlParam(), 1003.ToSqlParam(), User.GetId().ToSqlParam()); + using SqlCommand cmd = new(formattedSQL_hist, conn); + cmd.CommandType = CommandType.Text; + + using SqlDataReader reader = cmd.ExecuteReader(); + if (reader.Read()) + { + bool outSuccess = reader.GetBoolean(0); + if (!outSuccess) + _logger.LogWarning( + "PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}", + envelope.Uuid); + } + } + #endregion + + return Ok(res); + } + + /// + /// + /// + /// + /// + public static bool IsBase64String(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return false; + + try + { + Convert.FromBase64String(input); + return true; + } + catch (FormatException) + { + return false; + } + } + +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeTypeController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeTypeController.cs new file mode 100644 index 00000000..e6fc6095 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/EnvelopeTypeController.cs @@ -0,0 +1,39 @@ +using MediatR; +using EnvelopeGenerator.Application.EnvelopeTypes.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace EnvelopeGenerator.GeneratorAPI.Controllers; + +/// +/// +/// +[ApiExplorerSettings(IgnoreApi = true)] +[Route("api/[controller]")] +[ApiController] +public class EnvelopeTypeController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IMediator _mediator; + + /// + /// + /// + /// + /// + public EnvelopeTypeController(ILogger logger, IMediator mediator) + { + _logger = logger; + _mediator = mediator; + } + + /// + /// + /// + /// + [HttpGet] + public async Task GetAllAsync() + { + var result = await _mediator.Send(new ReadEnvelopeTypesQuery()); + return Ok(result); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HistoryController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HistoryController.cs new file mode 100644 index 00000000..9d70a5c6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/HistoryController.cs @@ -0,0 +1,118 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using EnvelopeGenerator.Application.Histories.Queries; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Application.Common.Extensions; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Dieser Controller stellt Endpunkte für den Zugriff auf die Umschlaghistorie bereit. +/// +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class HistoryController : ControllerBase +{ + private readonly IMemoryCache _memoryCache; + + private readonly IMediator _mediator; + + /// + /// Konstruktor für den HistoryController. + /// + /// + /// + public HistoryController(IMemoryCache memoryCache, IMediator mediator) + { + _memoryCache = memoryCache; + _mediator = mediator; + } + + /// + /// Gibt alle möglichen Verweise auf alle möglichen Include in einem Verlaufsdatensatz zurück. (z. B. DocumentSigned bezieht sich auf Receiver.) + /// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können. + /// 1 - Sender: + /// Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 1* beginnen. + /// 2 - Receiver: + /// Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 2* beginnen. + /// 3 - System: + /// Historische Datensätze, die sich auf den allgemeinen Zustand des Umschlags beziehen. Diese haben Statuscodes, die mit 3* beginnen. + /// 4 - Unknown: + /// Ein unbekannter Datensatz weist auf einen möglichen Mangel oder eine Unstimmigkeit im Aktualisierungsprozess der Anwendung hin. + /// + /// + /// + [HttpGet("related")] + [Authorize] + public IActionResult GetReferenceTypes(ReferenceType? referenceType = null) + { + return referenceType is null + ? Ok(_memoryCache.GetEnumAsDictionary("gen.api", ReferenceType.Unknown)) + : Ok(referenceType.ToString()); + } + + /// + /// Gibt alle möglichen Include in einem Verlaufsdatensatz zurück. + /// Dies wird hinzugefügt, damit Client-Anwendungen sich selbst auf dem neuesten Stand halten können. + /// 1003: EnvelopeQueued + /// 1006: EnvelopeCompletelySigned + /// 1007: EnvelopeReportCreated + /// 1008: EnvelopeArchived + /// 1009: EnvelopeDeleted + /// 10007: EnvelopeRejected + /// 10009: EnvelopeWithdrawn + /// 2001: AccessCodeRequested + /// 2002: AccessCodeCorrect + /// 2003: AccessCodeIncorrect + /// 2004: DocumentOpened + /// 2005: DocumentSigned + /// 2006: DocumentForwarded + /// 2007: DocumentRejected + /// 2008: EnvelopeShared + /// 2009: EnvelopeViewed + /// 3001: MessageInvitationSent (Wird von Trigger verwendet) + /// 3002: MessageAccessCodeSent + /// 3003: MessageConfirmationSent + /// 3004: MessageDeletionSent + /// 3005: MessageCompletionSent + /// + /// + /// Abfrageparameter, der angibt, auf welche Referenz sich der Include bezieht. + /// 1 - Sender: Historische Datensätze, die sich auf den Include des Absenders beziehen. Sie haben Statuscodes, die mit 1* beginnen. + /// 2 - Receiver: Historische Datensätze über den Include der Empfänger. Diese haben Statuscodes, die mit 2* beginnen. + /// 3 - System: Diese werden durch Datenbank-Trigger aktualisiert und sind in den Tabellen EnvelopeHistory und EmailOut zu finden.Sie arbeiten + /// integriert mit der Anwendung EmailProfiler, um E-Mails zu versenden und haben die Codes, die mit 3* beginnen. + /// + /// Gibt die HTTP-Antwort zurück. + /// + [HttpGet("status")] + [Authorize] + public IActionResult GetEnvelopeStatus([FromQuery] EnvelopeStatus? status = null) + { + return status is null + ? Ok(_memoryCache.GetEnumAsDictionary("gen.api", Status.NonHist, Status.RelatedToFormApp)) + : Ok(status.ToString()); + } + + /// + /// Ruft die gesamte Umschlaghistorie basierend auf den angegebenen Abfrageparametern ab. + /// + /// Die Abfrageparameter, die die Filterkriterien für die Umschlaghistorie definieren. + /// + /// Eine Liste von Historieneinträgen, die den angegebenen Kriterien entsprechen, oder nur der letzte Eintrag. + /// Die Anfrage war erfolgreich, und die Umschlaghistorie wird zurückgegeben. + /// Die Anfrage war ungültig oder unvollständig. + /// Der Benutzer ist nicht authentifiziert. + /// Der Benutzer hat keine Berechtigung, auf die Ressource zuzugreifen. + /// Ein unerwarteter Fehler ist aufgetreten. + [HttpGet] + [Authorize] + public async Task GetAllAsync([FromQuery] ReadHistoryQuery historyQuery, CancellationToken cancel) + { + var history = await _mediator.Send(historyQuery, cancel).ThrowIfEmpty(Exceptions.NotFound); + return Ok((historyQuery.OnlyLast) ? history.MaxBy(h => h.AddedWhen) : history); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/Interfaces/IAuthController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/Interfaces/IAuthController.cs new file mode 100644 index 00000000..92ac56ca --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/Interfaces/IAuthController.cs @@ -0,0 +1,38 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; + +namespace EnvelopeGenerator.Server.Controllers.Interfaces; + +/// +/// +/// +public interface IAuthController +{ + /// + /// + /// + IAuthorizationService AuthService { get; } + + /// + /// + /// + ClaimsPrincipal User { get; } +} + +/// +/// +/// +public static class AuthControllerExtensions +{ + /// + /// + /// + /// + /// + /// + public static async Task IsUserInPolicyAsync(this IAuthController controller, string policyName) + { + var result = await controller.AuthService.AuthorizeAsync(controller.User, policyName); + return result.Succeeded; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/LocalizationController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/LocalizationController.cs new file mode 100644 index 00000000..6aa622c4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/LocalizationController.cs @@ -0,0 +1,121 @@ +using DigitalData.Core.API; +using EnvelopeGenerator.Application.Resources; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Localization; +using EnvelopeGenerator.Application.Resources; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Controller für die Verwaltung der Lokalisierung und Spracheinstellungen. +/// +[ApiExplorerSettings(IgnoreApi = true)] +[Route("api/[controller]")] +[ApiController] +public class LocalizationController : ControllerBase +{ + private static readonly Guid L_KEY = Guid.NewGuid(); + + private readonly ILogger _logger; + private readonly IStringLocalizer _mLocalizer; + private readonly IStringLocalizer _localizer; + private readonly IMemoryCache _cache; + + /// + /// Konstruktor für den . + /// + /// Logger für die Protokollierung. + /// Lokalisierungsdienst für Ressourcen. + /// Speicher-Cache für die Zwischenspeicherung von Daten. + /// Lokalisierungsdienst für Modelle. + public LocalizationController( + ILogger logger, + IStringLocalizer localizer, + IMemoryCache memoryCache, + IStringLocalizer _modelLocalizer) + { + _logger = logger; + _localizer = localizer; + _cache = memoryCache; + _mLocalizer = _modelLocalizer; + } + + /// + /// Ruft alle lokalisierten Daten ab. + /// + /// Eine Liste aller lokalisierten Daten. + [HttpGet] + public IActionResult GetAll() => Ok(_cache.GetOrCreate(Language ?? string.Empty + L_KEY, _ => _mLocalizer.ToDictionary())); + + /// + /// Ruft die aktuelle Sprache ab. + /// + /// Die aktuelle Sprache oder ein NotFound-Ergebnis, wenn keine Sprache gesetzt ist. + [HttpGet("lang")] + public IActionResult GetLanguage() => Language is null ? NotFound() : Ok(Language); + + /// + /// Setzt die Sprache. + /// + /// Die zu setzende Sprache. + /// Ein Ok-Ergebnis, wenn die Sprache erfolgreich gesetzt wurde, oder ein BadRequest-Ergebnis, wenn die Eingabe ungültig ist. + [HttpPost("lang")] + public IActionResult SetLanguage([FromQuery] string language) + { + if (string.IsNullOrEmpty(language)) + return BadRequest(); + + Language = language; + return Ok(); + } + + /// + /// Löscht die aktuelle Sprache. + /// + /// Ein Ok-Ergebnis, wenn die Sprache erfolgreich gelöscht wurde. + [HttpDelete("lang")] + public IActionResult DeleteLanguage() + { + Language = null; + return Ok(); + } + + /// + /// Eigenschaft für die Verwaltung der aktuellen Sprache über Cookies. + /// + private string? Language + { + get + { + var cookieValue = Request.Cookies[CookieRequestCultureProvider.DefaultCookieName]; + + if (string.IsNullOrEmpty(cookieValue)) + return null; + + var culture = CookieRequestCultureProvider.ParseCookieValue(cookieValue)?.Cultures[0]; + return culture?.Value ?? null; + } + set + { + if (value is null) + Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName); + else + { + var cookieOptions = new CookieOptions() + { + Expires = DateTimeOffset.UtcNow.AddYears(1), + Secure = false, + SameSite = SameSiteMode.Strict, + HttpOnly = true + }; + + Response.Cookies.Append( + CookieRequestCultureProvider.DefaultCookieName, + CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(value)), + cookieOptions); + } + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ReadOnlyController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ReadOnlyController.cs new file mode 100644 index 00000000..b00f17de --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ReadOnlyController.cs @@ -0,0 +1,91 @@ +using DigitalData.Core.Abstraction.Application.DTO; +using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly; +using EnvelopeGenerator.Application.Common.Interfaces.Services; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Server.Extensions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Manages read-only envelope sharing flows. +/// +[Route("api/[controller]")] +[ApiController] +public class ReadOnlyController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IEnvelopeReceiverReadOnlyService _readOnlyService; + private readonly IEnvelopeMailService _mailService; + private readonly IEnvelopeHistoryService _historyService; + + /// + /// Initializes a new instance of the class. + /// + public ReadOnlyController(ILogger logger, IEnvelopeReceiverReadOnlyService readOnlyService, IEnvelopeMailService mailService, IEnvelopeHistoryService historyService) + { + _logger = logger; + _readOnlyService = readOnlyService; + _mailService = mailService; + _historyService = historyService; + } + + /// + /// Creates a new read-only receiver for the current envelope. + /// + /// Creation payload. + [HttpPost] + [Authorize(Policy = AuthPolicy.Receiver)] + [Obsolete("Use MediatR")] + public async Task CreateAsync([FromBody] EnvelopeReceiverReadOnlyCreateDto createDto) + { + var authReceiverMail = User.ReceiverMail(); + if (authReceiverMail is null) + { + _logger.LogError("EmailAddress claim is not found in envelope-receiver-read-only creation process. Create DTO is:\n {dto}", JsonConvert.SerializeObject(createDto)); + return Unauthorized(); + } + + var envelopeId = User.EnvelopeId(); + + createDto.AddedWho = authReceiverMail; + createDto.EnvelopeId = envelopeId; + + var creationRes = await _readOnlyService.CreateAsync(createDto: createDto); + + if (creationRes.IsFailed) + { + _logger.LogNotice(creationRes); + return StatusCode(StatusCodes.Status500InternalServerError); + } + + var readRes = await _readOnlyService.ReadByIdAsync(creationRes.Data.Id); + if (readRes.IsFailed) + { + _logger.LogNotice(creationRes); + return StatusCode(StatusCodes.Status500InternalServerError); + } + + var newReadOnly = readRes.Data; + + return await _mailService.SendAsync(newReadOnly).ThenAsync(SuccessAsync: async _ => + { + var histRes = await _historyService.RecordAsync((int)createDto.EnvelopeId, createDto.AddedWho, EnvelopeStatus.EnvelopeShared); + if (histRes.IsFailed) + { + _logger.LogError("Although the envelope was sent as read-only, the EnvelopeShared history could not be saved. Create DTO:\n{createDto}", JsonConvert.SerializeObject(createDto)); + _logger.LogNotice(histRes.Notices); + } + + return Ok(); + }, + + Fail: (msg, ntc) => + { + _logger.LogNotice(ntc); + return StatusCode(StatusCodes.Status500InternalServerError); + }); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ReceiverController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ReceiverController.cs new file mode 100644 index 00000000..b7b6c417 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/ReceiverController.cs @@ -0,0 +1,48 @@ +using MediatR; +using EnvelopeGenerator.Application.Receivers.Queries; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Controller für die Verwaltung von Empfängern. +/// +/// +/// Dieser Controller bietet Endpunkte für das Abrufen von Empfängern basierend auf E-Mail-Adresse oder Signatur. +/// +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ReceiverController : ControllerBase +{ + private readonly IMediator _mediator; + + /// + /// Initialisiert eine neue Instanz des -Controllers. + /// + /// Mediator für Anfragen. + public ReceiverController(IMediator mediator) + { + _mediator = mediator; + } + + /// + /// Ruft eine Liste von Empfängern ab, basierend auf den angegebenen Abfrageparametern. + /// + /// Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur. + /// Eine Liste von Empfängern oder ein Fehlerstatus. + [HttpGet] + [Authorize(AuthenticationSchemes = AuthScheme.Sender)] + public async Task Get([FromQuery] ReadReceiverQuery? receiver = null, [FromQuery] bool onlyEmailAddress = false) + { + var result = await _mediator.Send(receiver ?? new ReadReceiverQuery()); + + if (result is null) + return NotFound(); + else if (onlyEmailAddress) + return Ok(result.Select(r => r.EmailAddress).ToList()); + else + return Ok(result); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs new file mode 100644 index 00000000..2c951983 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/SignatureController.cs @@ -0,0 +1,57 @@ +using EnvelopeGenerator.Server.Extensions; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Extensions; +using EnvelopeGenerator.Application.Documents.Queries; +using EnvelopeGenerator.Domain.Constants; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// +/// +[Authorize(Policy = AuthPolicy.Receiver)] +[ApiController] +[Route("api/[controller]")] +public class SignatureController : ControllerBase +{ + private readonly IMediator _mediator; + + /// + /// Initializes a new instance of . + /// + public SignatureController(IMediator mediator) + { + _mediator = mediator; + } + + //TODO: update to use signature query + /// + /// + /// + /// + /// + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpGet("{envelopeKey}")] + public async Task Get(string envelopeKey, CancellationToken cancel) + { + int envelopeId = User.EnvelopeId(); + + int receiverId = User.ReceiverId(); + + var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel); + + if (doc.Elements is not IEnumerable docSignatures) + return NotFound("Document is empty."); + + var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList(); + + if (rcvSignatures is null) + return NotFound("No signatures found for the current receiver."); + else + return Ok(rcvSignatures); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/TfaRegistrationController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/TfaRegistrationController.cs new file mode 100644 index 00000000..0e3a8f80 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/TfaRegistrationController.cs @@ -0,0 +1,129 @@ +using DigitalData.Core.Abstraction.Application.DTO; +using EnvelopeGenerator.Application.Common.Extensions; +using EnvelopeGenerator.Application.Common.Interfaces.Services; +using EnvelopeGenerator.Application.Resources; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Server.Models; +using Ganss.Xss; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Server.Controllers; + +/// +/// Exposes endpoints for registering and managing two-factor authentication for envelope receivers. +/// +[ApiController] +[Route("api/tfa")] +public class TfaRegistrationController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IEnvelopeReceiverService _envelopeReceiverService; + private readonly IAuthenticator _authenticator; + private readonly IReceiverService _receiverService; + private readonly TFARegParams _parameters; + private readonly IStringLocalizer _localizer; + + /// + /// Initializes a new instance of the class. + /// + public TfaRegistrationController( + ILogger logger, + IEnvelopeReceiverService envelopeReceiverService, + IAuthenticator authenticator, + IReceiverService receiverService, + IOptions tfaRegParamsOptions, + IStringLocalizer localizer) + { + _logger = logger; + _envelopeReceiverService = envelopeReceiverService; + _authenticator = authenticator; + _receiverService = receiverService; + _parameters = tfaRegParamsOptions.Value; + _localizer = localizer; + } + + /// + /// Generates registration metadata (QR code and deadline) for a receiver. + /// + /// Encoded envelope receiver id. + [Authorize] + [HttpGet("{envelopeReceiverId}")] + public async Task RegisterAsync(string envelopeReceiverId) + { + try + { + var (uuid, signature) = envelopeReceiverId.DecodeEnvelopeReceiverId(); + + if (uuid is null || signature is null) + { + _logger.LogEnvelopeError(uuid: uuid, signature: signature, message: _localizer.WrongEnvelopeReceiverId()); + return Unauthorized(new { message = _localizer.WrongEnvelopeReceiverId() }); + } + + var secretResult = await _envelopeReceiverService.ReadWithSecretByUuidSignatureAsync(uuid: uuid, signature: signature); + if (secretResult.IsFailed) + { + _logger.LogNotice(secretResult.Notices); + return NotFound(new { message = _localizer.WrongEnvelopeReceiverId() }); + } + + var envelopeReceiver = secretResult.Data; + + if (!envelopeReceiver.Envelope!.TFAEnabled) + return Unauthorized(new { message = _localizer.WrongAccessCode() }); + + var receiver = envelopeReceiver.Receiver; + receiver!.TotpSecretkey = _authenticator.GenerateTotpSecretKey(); + await _receiverService.UpdateAsync(receiver); + var totpQr64 = _authenticator.GenerateTotpQrCode(userEmail: receiver.EmailAddress, secretKey: receiver.TotpSecretkey).ToBase64String(); + + if (receiver.TfaRegDeadline is null) + { + receiver.TfaRegDeadline = _parameters.Deadline; + await _receiverService.UpdateAsync(receiver); + } + else if (receiver.TfaRegDeadline <= DateTime.Now) + { + return StatusCode(StatusCodes.Status410Gone, new { message = _localizer.WrongAccessCode() }); + } + + return Ok(new + { + envelopeReceiver.EnvelopeId, + envelopeReceiver.Envelope!.Uuid, + envelopeReceiver.Receiver!.Signature, + receiver.TfaRegDeadline, + TotpQR64 = totpQr64 + }); + } + catch (Exception ex) + { + _logger.LogEnvelopeError(envelopeReceiverId: envelopeReceiverId, exception: ex, message: _localizer.WrongEnvelopeReceiverId()); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() }); + } + } + + /// + /// Logs out the envelope receiver from cookie authentication. + /// + [Authorize(Policy = AuthPolicy.Receiver)] + [HttpPost("auth/logout")] + public async Task LogOutAsync() + { + try + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "{message}", ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = _localizer.UnexpectedError() }); + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Documentation/AuthProxyDocumentFilter.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Documentation/AuthProxyDocumentFilter.cs new file mode 100644 index 00000000..27ba9374 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Documentation/AuthProxyDocumentFilter.cs @@ -0,0 +1,123 @@ +using EnvelopeGenerator.Server.Models; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace EnvelopeGenerator.Server.Documentation; + +/// +/// +/// +public sealed class AuthProxyDocumentFilter : IDocumentFilter +{ + /// + /// + /// + /// + /// + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + AddLoginOperation(swaggerDoc, context); + AddEnvelopeReceiverLoginOperation(swaggerDoc, context); + } + + private static void AddLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + const string path = "/api/auth"; + + var loginSchema = context.SchemaGenerator.GenerateSchema(typeof(Login), context.SchemaRepository); + var loginExample = new OpenApiObject + { + ["password"] = new OpenApiString(""), + ["username"] = new OpenApiString("") + }; + + var operation = new OpenApiOperation + { + Summary = "Proxy login (auth-hub)", + Description = "Proxies the request to the auth service. Add query parameter `cookie=true|false`.", + Tags = [new() { Name = "Auth" }], + Parameters = + { + new OpenApiParameter + { + Name = "cookie", + In = ParameterLocation.Query, + Required = false, + Schema = new OpenApiSchema { Type = "boolean", Default = new OpenApiBoolean(true) }, + Example = new OpenApiBoolean(true), + Description = "If true, auth service sets the auth cookie." + } + }, + RequestBody = new OpenApiRequestBody + { + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample }, + ["multipart/form-data"] = new OpenApiMediaType { Schema = loginSchema, Example = loginExample } + } + }, + Responses = + { + ["200"] = new OpenApiResponse { Description = "OK (proxied response)" }, + ["401"] = new OpenApiResponse { Description = "Unauthorized" } + } + }; + + swaggerDoc.Paths[path] = new OpenApiPathItem + { + Operations = + { + [OperationType.Post] = operation + } + }; + } + + private static void AddEnvelopeReceiverLoginOperation(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + const string path = "/api/Auth/envelope-receiver/{key}"; + + var bodySchema = context.SchemaGenerator.GenerateSchema(typeof(EnvelopeReceiverLogin), context.SchemaRepository); + + var operation = new OpenApiOperation + { + Summary = "Envelope receiver login (auth-hub proxy)", + Description = "Proxies the envelope receiver login to the auth service. " + + "The `cookie` query parameter is always forwarded as `true` so the auth service sets the per-envelope cookie automatically.", + Tags = [new() { Name = "Auth" }], + Parameters = + { + new OpenApiParameter + { + Name = "key", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema { Type = "string" }, + Description = "The unique envelope receiver key." + } + }, + RequestBody = new OpenApiRequestBody + { + Required = false, + Content = + { + ["multipart/form-data"] = new OpenApiMediaType { Schema = bodySchema } + } + }, + Responses = + { + ["200"] = new OpenApiResponse { Description = "OK per-envelope cookie set by auth service." }, + ["401"] = new OpenApiResponse { Description = "Unauthorized invalid or missing access code." } + } + }; + + swaggerDoc.Paths[path] = new OpenApiPathItem + { + Operations = + { + [OperationType.Post] = operation + } + }; + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj new file mode 100644 index 00000000..328bbe8c --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/EnvelopeGenerator.Server.csproj @@ -0,0 +1,71 @@ + + + + net8.0 + enable + enable + true + EnvelopeGenerator.Server + + Digital Data GmbH + Digital Data GmbH + EnvelopeGenerator.Server + 1.0.1-beta + 1.0.1.0 + 1.0.1.0 + Copyright © 2026 Digital Data GmbH. All rights reserved. + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + + + + + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/ReceiverClaimExtensions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/ReceiverClaimExtensions.cs new file mode 100644 index 00000000..cc2611ab --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/ReceiverClaimExtensions.cs @@ -0,0 +1,96 @@ +using DigitalData.Auth.Claims; +using Microsoft.IdentityModel.JsonWebTokens; +using System.Security.Claims; + +namespace EnvelopeGenerator.Server.Extensions; + +/// +/// Provides helper methods for working with envelope-specific authentication claims. +/// +public static class ReceiverClaimExtensions +{ + /// + /// + /// + /// + /// + /// + /// + private static string GetRequiredClaimValue(this ClaimsPrincipal user, string claimType) + { + var value = user.FindFirstValue(claimType); + if (value is not null) + { + return value; + } + + var identity = user.Identity; + var principalName = identity?.Name ?? "(anonymous)"; + var authType = identity?.AuthenticationType ?? "(none)"; + var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}")); + var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}]."; + throw new InvalidOperationException(message); + } + + private static string GetRequiredClaimValue(this ClaimsPrincipal user, params string[] claimTypes) + { + foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct()) + { + var value = user.FindFirstValue(claimType); + if (!string.IsNullOrWhiteSpace(value)) + return value; + } + + var identity = user.Identity; + var principalName = identity?.Name ?? "(anonymous)"; + var authType = identity?.AuthenticationType ?? "(none)"; + var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}")); + var message = $"Required claim(s) '{string.Join("', '", claimTypes)}' are missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}]."; + throw new InvalidOperationException(message); + } + + /// + /// Gets the authenticated envelope UUID from the claims. + /// + public static string EnvelopeUuid(this ClaimsPrincipal user) + => user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeUuid); + + /// + /// Gets the authenticated receiver signature from the claims. + /// + public static string ReceiverSignature(this ClaimsPrincipal user) + => user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverSignature); + + /// + /// Gets the authenticated receiver email address from the claims. + /// + public static string ReceiverMail(this ClaimsPrincipal user) + => user.GetRequiredClaimValue(JwtRegisteredClaimNames.Email); + + /// + /// Gets the authenticated envelope identifier from the claims. + /// + public static int EnvelopeId(this ClaimsPrincipal user) + { + var envIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.EnvelopeId); + if (int.TryParse(envIdStr, out var envId)) + return envId; + else + throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.EnvelopeId}' is not a valid integer."); + } + + /// + /// Gets the authenticated receiver identifier from the claims. + /// + /// + /// + /// + public static int ReceiverId(this ClaimsPrincipal user) + { + var rcvIdStr = user.GetRequiredClaimValue(EnvelopeClaimNames.ReceiverId); + if (int.TryParse(rcvIdStr, out var rcvId)) + return rcvId; + else + throw new InvalidOperationException($"Claim '{EnvelopeClaimNames.ReceiverId}' is not a valid integer."); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/SenderClaimExtensions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/SenderClaimExtensions.cs new file mode 100644 index 00000000..44829fbe --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Extensions/SenderClaimExtensions.cs @@ -0,0 +1,95 @@ +using System.Security.Claims; + +namespace EnvelopeGenerator.Server.Extensions +{ + /// + /// Provides extension methods for extracting user information from a . + /// + public static class SenderClaimExtensions + { + private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, string claimType) + { + var value = user.FindFirstValue(claimType); + if (value is not null) + { + return value; + } + + var identity = user.Identity; + var principalName = identity?.Name ?? "(anonymous)"; + var authType = identity?.AuthenticationType ?? "(none)"; + var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}")); + var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}]."; + throw new InvalidOperationException(message); + } + + private static string GetRequiredClaimOfSender(this ClaimsPrincipal user, params string[] claimTypes) + { + string? value = null; + + foreach (var claimType in claimTypes) + { + value = user.FindFirstValue(claimType); + if (value is not null) + return value; + } + + var identity = user.Identity; + var principalName = identity?.Name ?? "(anonymous)"; + var authType = identity?.AuthenticationType ?? "(none)"; + var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}")); + var message = $"Required claim among [{string.Join(", ", claimTypes)}] is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}]."; + throw new InvalidOperationException(message); + } + + /// + /// Retrieves the user's ID from the claims. Throws an exception if the ID is missing or invalid. + /// + /// The representing the user. + /// The user's ID as an integer. + /// Thrown if the user ID claim is missing or invalid. + public static int GetId(this ClaimsPrincipal user) + { + var idValue = user.GetRequiredClaimOfSender(ClaimTypes.NameIdentifier, "sub"); + + if (!int.TryParse(idValue, out var result)) + { + throw new InvalidOperationException("User ID claim is missing or invalid. This may indicate a misconfigured or forged JWT token."); + } + + return result; + } + + /// + /// Retrieves the username from the claims. + /// + /// The representing the user. + /// The username as a string. + public static string GetUsername(this ClaimsPrincipal user) + => user.GetRequiredClaimOfSender(ClaimTypes.Name); + + /// + /// Retrieves the user's surname (last name) from the claims. + /// + /// The representing the user. + /// The surname as a string. + public static string GetName(this ClaimsPrincipal user) + => user.GetRequiredClaimOfSender(ClaimTypes.Surname); + + /// + /// Retrieves the user's given name (first name) from the claims. + /// + /// The representing the user. + /// The given name as a string. + public static string GetPrename(this ClaimsPrincipal user) + => user.GetRequiredClaimOfSender(ClaimTypes.GivenName); + + /// + /// Retrieves the user's email address from the claims. + /// + /// The representing the user. + /// The email address as a string. + public static string GetEmail(this ClaimsPrincipal user) + => user.GetRequiredClaimOfSender(ClaimTypes.Email); + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Handlers/SenderAuthCookieHandler.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Handlers/SenderAuthCookieHandler.cs new file mode 100644 index 00000000..27ea8c08 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Handlers/SenderAuthCookieHandler.cs @@ -0,0 +1,33 @@ +namespace EnvelopeGenerator.Server.Handlers; + +/// +/// A that forwards the incoming HTTP request's +/// Cookie header to all outgoing calls +/// made by Blazor Server components. +/// +/// Problem it solves: +/// Blazor Server runs on the server process. When a component calls an API endpoint +/// that requires cookie-based JWT authentication (AuthScheme.Sender), the HttpClient +/// does not automatically include the browser's cookies — those only travel with +/// browser-initiated requests. This handler copies the Cookie header from the +/// current into every outgoing request +/// so that the API's JwtBearer OnMessageReceived callback can extract the token. +/// +/// Thread safety: +/// The handler is registered as Transient and is resolved per-request by the +/// IHttpClientFactory pipeline, so there is no shared state between requests. +/// +public class SenderAuthCookieHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler +{ + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var cookieHeader = httpContextAccessor.HttpContext?.Request.Headers["Cookie"].ToString(); + + if (!string.IsNullOrWhiteSpace(cookieHeader)) + request.Headers.TryAddWithoutValidation("Cookie", cookieHeader); + + return base.SendAsync(request, cancellationToken); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Jenkinsfile b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Jenkinsfile new file mode 100644 index 00000000..3546ad67 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Jenkinsfile @@ -0,0 +1,10 @@ +pipeline { + agent any + stages { + stage('Build') { + steps { + sh 'dotnet build' + } + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Middleware/ExceptionHandlingMiddleware.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 00000000..8f4b252c --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,84 @@ +namespace EnvelopeGenerator.Server.Middleware; + +using DigitalData.Core.Exceptions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Text.Json; + +/// +/// Middleware for handling exceptions globally in the application. +/// Captures exceptions thrown during the request pipeline execution, +/// logs them, and returns an appropriate HTTP response with a JSON error message. +/// +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The next middleware in the request pipeline. + /// The logger instance for logging exceptions. + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + /// + /// Invokes the middleware to handle the HTTP request. + /// + /// The HTTP context of the current request. + /// A task that represents the asynchronous operation. + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); // Continue down the pipeline + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex, _logger); + } + } + + /// + /// Handles exceptions by logging them and writing an appropriate JSON response. + /// + /// The HTTP context of the current request. + /// The exception that occurred. + /// The logger instance for logging the exception. + /// A task that represents the asynchronous operation. + private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger) + { + context.Response.ContentType = "application/json"; + + string message; + + switch (exception) + { + case BadRequestException badRequestEx: + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + message = badRequestEx.Message; + break; + + case NotFoundException notFoundEx: + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + message = notFoundEx.Message; + break; + + default: + logger.LogError(exception, "Unhandled exception occurred."); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + message = "An unexpected error occurred."; + break; + } + + await context.Response.WriteAsync(JsonSerializer.Serialize(new + { + message + })); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Auth.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Auth.cs new file mode 100644 index 00000000..e08f727d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Auth.cs @@ -0,0 +1,15 @@ +namespace EnvelopeGenerator.Server.Models; + +[Obsolete("Use auth DTO")] +public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default) +{ + public bool HasAccessCode => AccessCode is not null; + + public bool HasSmsCode => SmsCode is not null; + + public bool HasAuthenticatorCode => AuthenticatorCode is not null; + + public bool HasMulti => new[] { HasAccessCode, HasSmsCode, HasAuthenticatorCode }.Count(state => state) > 1; + + public bool HasNone => !(HasAccessCode || HasSmsCode || HasAuthenticatorCode); +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/AuthTokenKeys.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/AuthTokenKeys.cs new file mode 100644 index 00000000..c62008dc --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/AuthTokenKeys.cs @@ -0,0 +1,28 @@ +namespace EnvelopeGenerator.Server.Models; + +/// +/// Represents the keys and default values used for authentication token handling +/// within the Envelope Generator Server. +/// +public class AuthTokenKeys +{ + /// + /// Gets the name of the cookie used to store the authentication token. + /// + public string Cookie { get; init; } = "AuthToken"; + + /// + /// Gets the name of the query string parameter used to pass the authentication token. + /// + public string QueryString { get; init; } = "AuthToken"; + + /// + /// Gets the expected issuer value for the authentication token. + /// + public string Issuer { get; init; } = "auth.digitaldata.works"; + + /// + /// Gets the expected audience value for the authentication token. + /// + public string Audience { get; init; } = "sign-flow.digitaldata.works"; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ConnectionString.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ConnectionString.cs new file mode 100644 index 00000000..8d65b047 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ConnectionString.cs @@ -0,0 +1,12 @@ +namespace EnvelopeGenerator.Server.Models; + +/// +/// Represents the database connection string for dependency injection. +/// +public class ConnectionString +{ + /// + /// The database connection string value. + /// + public string Value { get; set; } = string.Empty; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ContactLink.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ContactLink.cs new file mode 100644 index 00000000..619e3ce4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ContactLink.cs @@ -0,0 +1,60 @@ +namespace EnvelopeGenerator.Server.Models +{ + /// + /// Represents a hyperlink for contact purposes with various HTML attributes. + /// + public class ContactLink + { + /// + /// Gets or sets the label of the hyperlink. + /// + public string Label { get; init; } = "Contact"; + + /// + /// Gets or sets the URL that the hyperlink points to. + /// + public string Href { get; set; } = string.Empty; + + /// + /// Gets or sets the target where the hyperlink should open. + /// Commonly used values are "_blank", "_self", "_parent", "_top". + /// + public string Target { get; set; } = "_blank"; + + /// + /// Gets or sets the relationship of the linked URL as space-separated link types. + /// Examples include "nofollow", "noopener", "noreferrer". + /// + public string Rel { get; set; } = string.Empty; + + /// + /// Gets or sets the filename that should be downloaded when clicking the hyperlink. + /// This attribute will only have an effect if the href attribute is set. + /// + public string Download { get; set; } = string.Empty; + + /// + /// Gets or sets the language of the linked resource. Useful when linking to + /// content in another language. + /// + public string HrefLang { get; set; } = "en"; + + /// + /// Gets or sets the MIME type of the linked URL. Helps browsers to handle + /// the type correctly when the link is clicked. + /// + public string Type { get; set; } = string.Empty; + + /// + /// Gets or sets additional information about the hyperlink, typically viewed + /// as a tooltip when the mouse hovers over the link. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets an identifier for the hyperlink, unique within the HTML document. + /// + public string Id { get; set; } = string.Empty; + } + +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Culture.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Culture.cs new file mode 100644 index 00000000..4ded33c6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Culture.cs @@ -0,0 +1,17 @@ +using System.Globalization; + +namespace EnvelopeGenerator.Server.Models; + +public class Culture +{ + private string _language = string.Empty; + public string Language { get => _language; + init { + _language = value; + Info = new(value); + } + } + public string FIClass { get; init; } = string.Empty; + + public CultureInfo? Info { get; init; } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Cultures.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Cultures.cs new file mode 100644 index 00000000..cf5653fd --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Cultures.cs @@ -0,0 +1,12 @@ +namespace EnvelopeGenerator.Server.Models; + +public class Cultures : List +{ + public IEnumerable Languages => this.Select(c => c.Language); + + public IEnumerable FIClasses => this.Select(c => c.FIClass); + + public Culture Default => this.First(); + + public Culture? this[string? language] => language is null ? null : this.Where(c => c.Language == language).FirstOrDefault(); +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/CustomImages.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/CustomImages.cs new file mode 100644 index 00000000..862edf8d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/CustomImages.cs @@ -0,0 +1,6 @@ +namespace EnvelopeGenerator.Server.Models; + +public class CustomImages : Dictionary +{ + public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new(); +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/EnvelopeReceiverLogin.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/EnvelopeReceiverLogin.cs new file mode 100644 index 00000000..5455b119 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/EnvelopeReceiverLogin.cs @@ -0,0 +1,7 @@ +namespace EnvelopeGenerator.Server.Models; + +/// +/// Request body for the envelope-receiver login endpoint. +/// +/// The access code sent to the receiver. +public record EnvelopeReceiverLogin(string? AccessCode = null); diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ErrorViewModel.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ErrorViewModel.cs new file mode 100644 index 00000000..a2aaaad9 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/ErrorViewModel.cs @@ -0,0 +1,10 @@ +namespace EnvelopeGenerator.Server.Models; + +public class ErrorViewModel +{ + public string Title { get; init; } = "404"; + + public string Subtitle { get; init; } = "Hmmm..."; + + public string Body { get; init; } = "It looks like one of the developers fell asleep"; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Image.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Image.cs new file mode 100644 index 00000000..d2e7233b --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Image.cs @@ -0,0 +1,10 @@ +namespace EnvelopeGenerator.Server.Models; + +public class Image +{ + public string Src { get; init; } = string.Empty; + + public Dictionary Classes { get; init; } = new(); + + public string GetClassIn(string page) => Classes.TryGetValue(page, out var cls) && cls is not null ? cls : string.Empty; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Login.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Login.cs new file mode 100644 index 00000000..388593db --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/Login.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace EnvelopeGenerator.Server.Models; + +/// +/// Repräsentiert ein Login-Modell mit erforderlichem Passwort und optionaler ID und Benutzername. +/// +/// Das erforderliche Passwort für das Login. +/// Die optionale ID des Benutzers. +/// Der optionale Benutzername. +public record Login([Required] string Password, int? UserId = null, string? Username = null) +{ +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/MainViewModel.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/MainViewModel.cs new file mode 100644 index 00000000..f2fd4748 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/MainViewModel.cs @@ -0,0 +1,6 @@ +namespace EnvelopeGenerator.Server.Models; + +public class MainViewModel +{ + public string? Title { get; init; } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Annotation.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Annotation.cs new file mode 100644 index 00000000..9b5bd7c7 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Annotation.cs @@ -0,0 +1,93 @@ +using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; +using System.Text.Json.Serialization; + +namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; + +public record Annotation : IAnnotation +{ + public required string Name { get; init; } + + #region Bound Annotation + [JsonIgnore] + public string? HorBoundAnnotName { get; init; } + + [JsonIgnore] + public string? VerBoundAnnotName { get; init; } + #endregion + + #region Layout + [JsonIgnore] + public double? MarginLeft { get; set; } + + [JsonIgnore] + public double MarginLeftRatio { get; init; } = 1; + + [JsonIgnore] + public double? MarginTop { get; set; } + + [JsonIgnore] + public double MarginTopRatio { get; init; } = 1; + + public double? Width { get; set; } + + [JsonIgnore] + public double WidthRatio { get; init; } = 1; + + public double? Height { get; set; } + + [JsonIgnore] + public double HeightRatio { get; init; } = 1; + #endregion + + #region Position + public double Left => (MarginLeft ?? 0) + (HorBoundAnnot?.HorBoundary ?? 0); + + public double Top => (MarginTop ?? 0) + (VerBoundAnnot?.VerBoundary ?? 0); + #endregion + + #region Boundary + [JsonIgnore] + public double HorBoundary => Left + (Width ?? 0); + + [JsonIgnore] + public double VerBoundary => Top + (Height ?? 0); + #endregion + + #region BoundAnnot + [JsonIgnore] + public Annotation? HorBoundAnnot { get; set; } + + [JsonIgnore] + public Annotation? VerBoundAnnot { get; set; } + #endregion + + public Color? BackgroundColor { get; init; } + + #region Border + public Color? BorderColor { get; init; } + + public string? BorderStyle { get; init; } + + public int? BorderWidth { get; set; } + #endregion + + [JsonIgnore] + internal Annotation Default + { + set + { + // To set null value, annotation must have null (0) value but null must has non-null value + if (MarginLeft == null && value.MarginLeft != null) + MarginLeft = value.MarginLeft * MarginLeftRatio; + + if (MarginTop == null && value.MarginTop != null) + MarginTop = value.MarginTop * MarginTopRatio; + + if (Width == null && value.Width != null) + Width = value.Width * WidthRatio; + + if (Height == null && value.Height != null) + Height = value.Height * HeightRatio; + } + } +}; \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/AnnotationParams.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/AnnotationParams.cs new file mode 100644 index 00000000..4b9c8114 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/AnnotationParams.cs @@ -0,0 +1,80 @@ +using EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; +using System.Text.Json.Serialization; + +namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; + +public class AnnotationParams +{ + public AnnotationParams() + { + _AnnotationJSObjectInitor = new(CreateAnnotationJSObject); + } + + public Background? Background { get; init; } + + #region Annotation + [JsonIgnore] + public Annotation? DefaultAnnotation { get; init; } + + private readonly List _annots = new List(); + + public bool TryGet(string name, out Annotation annotation) + { +#pragma warning disable CS8601 // Possible null reference assignment. + annotation = _annots.FirstOrDefault(a => a.Name == name); +#pragma warning restore CS8601 // Possible null reference assignment. + return annotation is not null; + } + + public required IEnumerable Annotations + { + get => _annots; + init + { + _annots = value.ToList(); + + if (DefaultAnnotation is not null) + foreach (var annot in _annots) + annot.Default = DefaultAnnotation; + + for (int i = 0; i < _annots.Count; i++) + { + #region set bound annotations + // horizontal + if (_annots[i].HorBoundAnnotName is string horBoundAnnotName) + if (TryGet(horBoundAnnotName, out var horBoundAnnot)) + _annots[i].HorBoundAnnot = horBoundAnnot; + else + throw new InvalidOperationException($"{horBoundAnnotName} added as bound anotation. However, it is not defined."); + + // vertical + if (_annots[i].VerBoundAnnotName is string verBoundAnnotName) + if (TryGet(verBoundAnnotName, out var verBoundAnnot)) + _annots[i].VerBoundAnnot = verBoundAnnot; + else + throw new InvalidOperationException($"{verBoundAnnotName} added as bound anotation. However, it is not defined."); + #endregion + } + } + } + #endregion + + #region AnnotationJSObject + private Dictionary CreateAnnotationJSObject() + { + var dict = _annots.ToDictionary(a => a.Name.ToLower(), a => a as IAnnotation); + + if (Background is not null) + { + Background.Locate(_annots); + dict.Add(Background.Name.ToLower(), Background); + } + + return dict; + } + + private readonly Lazy> _AnnotationJSObjectInitor; + + public Dictionary AnnotationJSObject => _AnnotationJSObjectInitor.Value; + #endregion +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Background.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Background.cs new file mode 100644 index 00000000..6990d239 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Background.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; + +namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; + +/// +/// The Background is an annotation for the PSPDF Kit. However, it has no function. +/// It is only the first annotation as a background for other annotations. +/// +public record Background : IAnnotation +{ + [JsonIgnore] + public double Margin { get; init; } + + public string Name { get; } = "Background"; + + public double? Width { get; set; } + + public double? Height { get; set; } + + public double Left { get; set; } + + public double Top { get; set; } + + public Color? BackgroundColor { get; init; } + + #region Border + public Color? BorderColor { get; init; } + + public string? BorderStyle { get; init; } + + public int? BorderWidth { get; set; } + #endregion + + public void Locate(IEnumerable annotations) + { + // set Top + if (annotations.MinBy(a => a.Top)?.Top is double minTop) + Top = minTop; + + // set Left + if (annotations.MinBy(a => a.Left)?.Left is double minLeft) + Left = minLeft; + + // set Width + if(annotations.MaxBy(a => a.GetRight())?.GetRight() is double maxRight) + Width = maxRight - Left; + + // set Height + if (annotations.MaxBy(a => a.GetBottom())?.GetBottom() is double maxBottom) + Height = maxBottom - Top; + + // add margins + Top -= Margin; + Left -= Margin; + Width += Margin * 2; + Height += Margin * 2; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Color.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Color.cs new file mode 100644 index 00000000..0dec55e5 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Color.cs @@ -0,0 +1,10 @@ +namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; + +public record Color +{ + public int R { get; init; } = 0; + + public int G { get; init; } = 0; + + public int B { get; init; } = 0; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Extensions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Extensions.cs new file mode 100644 index 00000000..f0c68d1d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/Extensions.cs @@ -0,0 +1,8 @@ +namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; + +public static class Extensions +{ + public static double GetRight(this IAnnotation annotation) => annotation.Left + annotation?.Width ?? 0; + + public static double GetBottom(this IAnnotation annotation) => annotation.Top + annotation?.Height ?? 0; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/IAnnotation.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/IAnnotation.cs new file mode 100644 index 00000000..940f729a --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/PsPdfKitAnnotation/IAnnotation.cs @@ -0,0 +1,22 @@ +namespace EnvelopeGenerator.Server.Models.PsPdfKitAnnotation; + +public interface IAnnotation +{ + string Name { get; } + + double? Width { get; } + + double? Height { get; } + + double Left { get; } + + double Top { get; } + + Color? BackgroundColor { get; } + + Color? BorderColor { get; } + + string? BorderStyle { get; } + + int? BorderWidth { get; } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/TFARegParams.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/TFARegParams.cs new file mode 100644 index 00000000..3b3ca77f --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Models/TFARegParams.cs @@ -0,0 +1,17 @@ +namespace EnvelopeGenerator.Server.Models; + +/// +/// Represents the parameters for two-factor authentication (2FA) registration. +/// +public class TFARegParams +{ + /// + /// The maximum allowed time for completing the registration process. + /// + public TimeSpan TimeLimit { get; init; } = new(0, 30, 0); + + /// + /// The deadline for registration, calculated as the current time plus the . + /// + public DateTime Deadline => DateTime.Now.AddTicks(TimeLimit.Ticks); +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs new file mode 100644 index 00000000..530e555a --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs @@ -0,0 +1,18 @@ +namespace EnvelopeGenerator.Server.Options; + +/// +/// Configuration options for distributed caching. +/// +public sealed class CacheOptions +{ + /// + /// Configuration section name in appsettings.json. + /// + public const string SectionName = "Cache"; + + /// + /// Signature cache expiration time. + /// If null, signatures will not expire automatically. + /// + public TimeSpan? SignatureCacheExpiration { get; set; } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs new file mode 100644 index 00000000..0a0769c6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs @@ -0,0 +1,418 @@ +using EnvelopeGenerator.Server.Components; +using EnvelopeGenerator.Server.Models; +using EnvelopeGenerator.Server.Options; +using DevExpress.Blazor; +using EnvelopeGenerator.Server.Client.Services; +using DigitalData.Core.API; +using DigitalData.Core.Application; +using EnvelopeGenerator.Infrastructure; +using EnvelopeGenerator.Domain.Constants; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Localization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using System.Globalization; +using Scalar.AspNetCore; +using Microsoft.OpenApi.Models; +using DigitalData.UserManager.DependencyInjection; +using EnvelopeGenerator.Application; +using DigitalData.Auth.Client; +using DigitalData.Core.Abstractions; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using DigitalData.Core.Abstractions.Security.Extensions; +using NLog.Web; +using NLog; +using DigitalData.Auth.Claims; +using EnvelopeGenerator.Server; + +var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); +logger.Info("EnvelopeGenerator.Server logging initialized!"); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + // Load YARP configuration from yarp.json + builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true); + + builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + + if (!builder.Environment.IsDevelopment()) + { + builder.Logging.ClearProviders(); + builder.Host.UseNLog(); + } + + var config = builder.Configuration; + + var deferredProvider = new DeferredServiceProvider(); + + // Add Blazor services + builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddInteractiveWebAssemblyComponents(); + + // Add API Controllers + builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; + }); + builder.Services.AddHttpClient(); + + // YARP Reverse Proxy (for forwarding auth requests to AuthHub) + builder.Services.AddReverseProxy() + .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); + + // HttpContextAccessor needed for SSR HttpClient configuration + builder.Services.AddHttpContextAccessor(); + + // Named HttpClient for internal API calls + // SenderAuthCookieHandler forwards the browser's Cookie header so that + // Blazor Server components can call cookie-authenticated endpoints (AuthScheme.Sender). + builder.Services.AddTransient(); + builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) => + { + var httpContextAccessor = sp.GetRequiredService(); + var request = httpContextAccessor.HttpContext?.Request; + + if (request != null) + { + // Set base address to current host for SSR scenarios + client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}"); + } + }) + .AddHttpMessageHandler(); + + // CORS Policy + var allowedOrigins = config.GetSection("AllowedOrigins").Get() ?? + throw new InvalidOperationException("AllowedOrigins section is missing in the configuration."); + builder.Services.AddCors(options => + { + options.AddPolicy("AllowSpecificOriginsPolicy", builder => + { + builder.WithOrigins(allowedOrigins) + .SetIsOriginAllowedToAllowWildcardSubdomains() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); + }); + + // Swagger/OpenAPI + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "signFLOW Absender-API", + Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschl�gen in der signFLOW-Anwendung.", + Contact = new OpenApiContact + { + Name = "Digital Data GmbH", + Url = new Uri("https://digitaldata.works/digitale-signatur#kontakt"), + Email = "info-flow@digitaldata.works" + }, + }); + + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JWT-Autorisierungs-Header unter Verwendung des Bearer-Schemas.", + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + + var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml"); + foreach (var xmlFile in xmlFiles) + { + options.IncludeXmlComments(xmlFile); + } + }); + + // Database Context + var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue("UseDbMigration"); + var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default"; + var connStr = config.GetConnectionString(cnnStrName) + ?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration."); + + builder.Services.Configure(cs => cs.Value = connStr); + + builder.Services.AddDbContext(options => options.UseSqlServer(connStr)); + + // Authentication - AuthHub + builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams")); + + var authTokenKeys = config.GetOrDefault(); + + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(AuthScheme.Sender, opt => + { + opt.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => + { + var clientParams = deferredProvider.GetOptions(); + var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience); + return [publicKey.SecurityKey]; + }, + ValidateIssuer = true, + ValidIssuer = authTokenKeys.Issuer, + ValidateAudience = true, + ValidAudience = authTokenKeys.Audience, + }; + + opt.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + if (context.Token is null) + { + if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null) + context.Token = cookieToken; + else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken)) + context.Token = queryStrToken; + } + return Task.CompletedTask; + } + }; + }) + .AddJwtBearer(AuthScheme.Receiver, opt => + { + opt.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => + { + var clientParams = deferredProvider.GetOptions(); + var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience); + return [publicKey.SecurityKey]; + }, + ValidateIssuer = true, + ValidIssuer = authTokenKeys.Issuer, + ValidateAudience = true, + ValidAudience = authTokenKeys.Audience, + }; + + opt.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries); + var envelopeKey = paths?.LastOrDefault(); + + if (envelopeKey is not null) + { + var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey); + if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null) + context.Token = cookieToken; + } + + return Task.CompletedTask; + }, + OnTokenValidated = context => + { + var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries); + var envelopeKey = paths?.LastOrDefault(); + + var sub = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value + ?? context.Principal?.FindFirst("sub")?.Value; + + if (envelopeKey is null || sub != envelopeKey) + context.Fail("Envelope key in the path does not match the token subject."); + + return Task.CompletedTask; + } + }; + }); + + // Cookie Authentication + builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.Cookie.SameSite = SameSiteMode.Strict; + options.LoginPath = "/api/auth/login"; + options.LogoutPath = "/api/auth/logout"; + options.SlidingExpiration = true; + }); + + // Authorization Policies + builder.Services.AddAuthorizationBuilder() + .AddPolicy(AuthPolicy.Sender, policy => policy + .RequireRole(Role.Sender) + .AddAuthenticationSchemes(AuthScheme.Sender)) + .AddPolicy(AuthPolicy.Receiver, policy => policy + .AddAuthenticationSchemes(AuthScheme.Receiver) + .RequireAuthenticatedUser() + .RequireRole(Role.Receiver.Full, "receiver")) + .AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA)); + + // User Manager +#pragma warning disable CS0618 + builder.Services.AddUserManager(); +#pragma warning restore CS0618 + + // LDAP Directory Search + builder.ConfigureBySection(); + builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions")); + + // Localization + builder.Services.AddCookieBasedLocalizer(); + + // Cache options + builder.Services.Configure(config.GetSection(CacheOptions.SectionName)); + + // Distributed Cache - SQL Server + builder.Services.AddDistributedSqlServerCache(options => + { + config.GetSection("Cache:SqlServer").Bind(options); + + if (string.IsNullOrWhiteSpace(options.ConnectionString)) + { + options.ConnectionString = connStr; + } + }); + + // Envelope Generator Infrastructure & Application Services +#pragma warning disable CS0618 + builder.Services + .AddEnvelopeGeneratorInfrastructureServices(opt => + { + opt.AddDbTriggerParams(config); + opt.AddDbContext((provider, options) => + { + var logger = provider.GetRequiredService>(); + options.UseSqlServer(connStr) + .LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace) + .EnableSensitiveDataLogging() + .EnableDetailedErrors(); + }); + opt.AddSQLExecutor(executor => executor.ConnectionString = connStr); + }) + .AddEnvelopeGeneratorServices(config); +#pragma warning restore CS0618 + + // Business Services (Server specific) + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + + // EnvelopeService with HttpClient factory (for SSR scenarios) + builder.Services.AddScoped(); + + // DocReceiverElementService (SignatureService alternative) + builder.Services.AddScoped(); + + // SSR Authentication Service (for Envelope Receiver pages) + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // DevExpress Server-Side Services (CRITICAL for DxPdfViewer) + builder.Services.AddDevExpressBlazor(); + builder.Services.AddDevExpressServerSideBlazorPdfViewer(); + + // PdfSharp font resolver — required for .NET 8 (no system font access without it) + PdfSharp.Fonts.GlobalFontSettings.FontResolver = + EnvelopeGenerator.Server.Services.PdfSharpFontResolver.Instance; + + // Configuration Options + builder.Services.Configure( + builder.Configuration.GetSection("ApiOptions")); + builder.Services.Configure( + builder.Configuration.GetSection("PdfViewerOptions")); + + var app = builder.Build(); + + deferredProvider.Factory = () => app.Services; + + // Exception handling middleware for API controllers + app.UseMiddleware(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + app.UseSwagger(); + app.UseSwaggerUI(); + app.MapScalarApiReference(); + } + else + { + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); + } + + // Set CORS policy + app.UseCors("AllowSpecificOriginsPolicy"); + + // Localization + string[] supportedCultureNames = ["de-DE", "en-US"]; + IList list = [.. supportedCultureNames.Select(cn => new CultureInfo(cn))]; + var cultureInfo = list.FirstOrDefault() ?? throw new InvalidOperationException("There is no supported culture."); + var requestLocalizationOptions = new RequestLocalizationOptions + { + SupportedCultures = list, + SupportedUICultures = list + }; + requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider()); + app.UseRequestLocalization(requestLocalizationOptions); + + app.UseHttpsRedirection(); + + app.UseDefaultFiles(); + app.UseStaticFiles(); + app.UseAntiforgery(); + + app.UseAuthentication(); + app.UseAuthorization(); + + // API Controllers (map before Blazor routing) + app.MapControllers(); + + // YARP Reverse Proxy - forwards unmatched requests to configured backends + app.MapReverseProxy(); + + // Blazor routing + app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(EnvelopeGenerator.Server.Client._Imports).Assembly); + + app.Run(); +} +catch (Exception ex) +{ + logger.Error(ex, "Stopped program because of exception"); + throw; +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/PublishProfiles/IISProfileNet8.pubxml b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/PublishProfiles/IISProfileNet8.pubxml new file mode 100644 index 00000000..f2612dfd --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/PublishProfiles/IISProfileNet8.pubxml @@ -0,0 +1,23 @@ + + + + + Package + Release + Any CPU + + true + false + 5e0e17c0-ff5a-4246-bf87-1add85376a27 + M:\App&Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.Server.zip + true + EnvelopeGenerator + <_TargetId>IISWebDeployPackage + net8.0 + true + win-x64 + Production + + \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json new file mode 100644 index 00000000..39f160ad --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Properties/launchSettings.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5131", + "sslPort": 8088 + } + }, + "profiles": { + "https (Blazor UI)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:8088;http://localhost:5131", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" + }, + "https (Swagger API)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:8088;http://localhost:5131", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http (Development)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5131", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md new file mode 100644 index 00000000..4f63e138 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md @@ -0,0 +1,225 @@ +# EnvelopeGenerator.Server — Publish & Deployment Guide + +## Inhaltsverzeichnis + +1. [Unterschied zu einer normalen ASP.NET Core API](#unterschied-zu-einer-normalen-aspnet-core-api) +2. [Warum Self-Contained Publish?](#warum-self-contained-publish) +3. [Publish-Befehl (Terminal)](#publish-befehl-terminal) +4. [IIS-Konfiguration](#iis-konfiguration) +5. [Verzeichnisstruktur nach dem Publish](#verzeichnisstruktur-nach-dem-publish) +6. [Häufige Fehler](#häufige-fehler) + +--- + +## Unterschied zu einer normalen ASP.NET Core API + +`EnvelopeGenerator.Server` ist **keine** gewöhnliche ASP.NET Core Web API. Es handelt sich um eine **Blazor Auto (Server + WebAssembly Hybrid)**-Anwendung. + +| Merkmal | Normale ASP.NET Core API | EnvelopeGenerator.Server (Blazor Auto) | +|---|---|---| +| Projekttyp | `Microsoft.NET.Sdk.Web` | `Microsoft.NET.Sdk.Web` + WASM Client | +| Frontend | Keins / Razor Pages | Blazor Server + Blazor WASM | +| WASM-Komponente | Nein | Ja (`EnvelopeGenerator.Server.Client`) | +| Framework-DLL-Bindung | Tolerant gegenüber Runtime-Versionen | **Strikt**: WASM erwartet exakte Assembly-Versionen | +| Publish ohne .NET auf Zielserver | Nicht nötig (FDD reicht meist) | **Self-Contained Pflicht** empfohlen | +| IIS Application Pool | `.NET CLR v4.0` oder `No Managed Code` | **Zwingend: `No Managed Code`** | +| Publish-Paketgröße | ~5–20 MB | **~500 MB** (enthält .NET Runtime) | +| `web.config processPath` | `dotnet` + `.dll` | **`.\EnvelopeGenerator.Server.exe`** | + +### Warum die WASM-Komponente den Unterschied macht + +Die WASM-Seite der Anwendung (`EnvelopeGenerator.Server.Client`) bindet Assemblies wie +`Microsoft.Extensions.DependencyInjection.Abstractions` in einer **fest definierten Version**. +Bei einem **Framework-Dependent Deployment** werden diese Assemblies nicht mitgeliefert und +müssen auf dem Zielserver vorhanden sein — in der exakt passenden Version. + +Fehlt die passende .NET-Runtime auf dem Zielserver, erscheint folgender Fehler beim Start: + +``` +Unhandled exception. System.IO.FileNotFoundException: +Could not load file or assembly +'Microsoft.Extensions.DependencyInjection.Abstractions, Version=8.0.0.0' +``` + +--- + +## Warum Self-Contained Publish? + +Beim **Self-Contained Deployment** werden **alle benötigten .NET Runtime-DLLs** in das +Ausgabeverzeichnis kopiert. Die Anwendung ist damit vollständig unabhängig von der auf dem +Zielserver installierten .NET-Version. + +| | Framework-Dependent | Self-Contained | +|---|---|---| +| .NET auf Zielserver nötig | Ja | **Nein** | +| Paketgröße | ~20 MB | ~500 MB | +| `runtimeconfig.json` | `frameworkVersion` vorhanden | `includedFrameworks` (Runtime eingebettet) | +| Fehleranfälligkeit auf Fremd-PC | Hoch | Minimal | + +--- + +## Publish-Befehl (Terminal) + +### Empfohlener Befehl (Self-Contained, win-x64) + +```bat +dotnet publish EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj ^ + -c Release ^ + -f net8.0 ^ + --self-contained true ^ + --runtime win-x64 ^ + -o .\publish-output +``` + +> Dieser Befehl muss vom **Solution-Root-Verzeichnis** aus ausgefuehrt werden. +> Alternativ: `publish.bat` im selben Verzeichnis wie diese README ausfuehren. + +### Parameter-Erklaerung + +| Parameter | Bedeutung | +|---|---| +| `-c Release` | Release-Konfiguration (optimiert, kein Debug-Code) | +| `-f net8.0` | Ziel-Framework explizit angeben (Pflicht, da `` mehrere Werte haben kann) | +| `--self-contained true` | Alle .NET Runtime-DLLs ins Ausgabeverzeichnis kopieren | +| `--runtime win-x64` | Zielplattform: Windows 64-Bit | +| `-o .\publish-output` | Ausgabeverzeichnis | + +### Doğrulama nach dem Publish + +Nach erfolgreichem Publish folgende Dateien im Ausgabeverzeichnis prüfen: + +```powershell +# Diese Dateien MÜSSEN vorhanden sein (Self-Contained-Nachweis): +Test-Path ".\publish-output\hostfxr.dll" # .NET Host +Test-Path ".\publish-output\coreclr.dll" # .NET Core Runtime +Test-Path ".\publish-output\Microsoft.Extensions.DependencyInjection.Abstractions.dll" +Test-Path ".\publish-output\EnvelopeGenerator.Server.exe" +Test-Path ".\publish-output\web.config" +``` + +Alle Ergebnisse müssen `True` sein. + +--- + +## IIS-Konfiguration + +> **WICHTIG:** Diese Einstellungen unterscheiden sich von einer normalen ASP.NET Core API +> und sind zwingend erforderlich. + +### 1. Application Pool — `No Managed Code` + +ASP.NET Core (und damit auch Blazor) verwaltet seinen eigenen Runtime-Lifecycle. +IIS darf **keinen** .NET CLR-Managed-Code-Kontext aktivieren. + +**Einstellung:** + +``` +IIS Manager + → Application Pools + → [Pool-Name der Anwendung] → Basic Settings + → .NET CLR Version: "No Managed Code" ← ZWINGEND +``` + +> **Fehler bei falscher Einstellung:** HTTP 500.30 — ASP.NET Core app failed to start +> (sc-win32-status: 574 in IIS-Logs) + +### 2. ASP.NET Core Module V2 + +Das IIS-Modul `AspNetCoreModuleV2` muss installiert sein. +Es wird über das **.NET Hosting Bundle** mitgeliefert. + +Prüfen: +``` +IIS Manager → Modules → "AspNetCoreModuleV2" vorhanden? +``` + +Falls nicht installiert: [.NET 8 Hosting Bundle herunterladen](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) +und nach der Installation IIS neu starten: + +```cmd +net stop was /y && net start w3svc +``` + +### 3. web.config — Korrekte Konfiguration für Self-Contained + +Nach dem Publish wird `web.config` automatisch generiert. Für Self-Contained muss sie so aussehen: + +```xml + + + + + + + + + + + +``` + +**Kritische Unterschiede zu Framework-Dependent:** + +| Eigenschaft | Framework-Dependent (FALSCH) | Self-Contained (RICHTIG) | +|---|---|---| +| `processPath` | `dotnet` | `.\EnvelopeGenerator.Server.exe` | +| `arguments` | `.\EnvelopeGenerator.Server.dll` | *(leer oder weggelassen)* | + +### 4. Berechtigungen für das `logs`-Verzeichnis + +Wenn `stdoutLogEnabled="true"` gesetzt wird (zur Fehlerdiagnose), muss das `logs`-Verzeichnis +existieren und der IIS-Prozess muss Schreibrechte haben: + +```powershell +New-Item -ItemType Directory -Path "C:\inetpub\wwwroot\\logs" -Force +icacls "C:\inetpub\wwwroot\\logs" /grant "IIS_IUSRS:(OI)(CI)F" +``` + +> Ohne dieses Verzeichnis kann die Anwendung bei aktiviertem Logging **nicht starten**. + +### 5. Application Pool Recycle nach Deployment + +Nach jedem Deployment den Application Pool neu starten: + +```cmd +# IIS Manager → Application Pools → [Pool] → Recycle +# oder per Kommandozeile (als Administrator): +%windir%\system32\inetsrv\appcmd recycle apppool /apppool.name:"" +``` + +--- + +## Verzeichnisstruktur nach dem Publish + +``` +publish-output\ +├── EnvelopeGenerator.Server.exe ← Startpunkt (Self-Contained) +├── EnvelopeGenerator.Server.dll ← Managed Assembly +├── EnvelopeGenerator.Server.runtimeconfig.json +├── EnvelopeGenerator.Server.deps.json +├── web.config ← IIS-Konfiguration (auto-generiert) +├── hostfxr.dll ← .NET Host (Self-Contained-Nachweis) +├── coreclr.dll ← .NET Core Runtime +├── Microsoft.Extensions.*.dll ← Framework-DLLs (jetzt enthalten!) +├── DevExpress.*.dll ← UI-Komponenten +├── wwwroot\ ← Statische Web-Assets +│ ├── _framework\ ← WASM-Binaries +│ └── ... +└── logs\ ← Stdout-Logs (manuell anlegen!) +``` + +--- + +## Häufige Fehler + +| Fehler | Ursache | Lösung | +|---|---|---| +| `FileNotFoundException: Microsoft.Extensions.DependencyInjection.Abstractions` | Framework-Dependent Publish auf Server ohne .NET 8 | Self-Contained Publish verwenden | +| `HTTP 500.30` in IIS | App startet nicht | Application Pool auf `No Managed Code` setzen | +| `HTTP 500.30` + `sc-win32-status: 574` | App Pool falsch oder `AspNetCoreModuleV2` fehlt | Pool prüfen + Hosting Bundle installieren | +| App startet per `.exe`, aber nicht in IIS | `web.config` hat noch `processPath="dotnet"` | `web.config` auf `.\EnvelopeGenerator.Server.exe` korrigieren | +| Logs-Verzeichnis fehlt → App startet nicht | `stdoutLogEnabled="true"` aber `logs\` existiert nicht | `logs\`-Ordner anlegen + IIS_IUSRS Schreibrecht geben | diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Resources/Invoice.pdf b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Resources/Invoice.pdf new file mode 100644 index 00000000..dbd52014 Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Resources/Invoice.pdf differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs new file mode 100644 index 00000000..9c2dac20 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs @@ -0,0 +1,91 @@ +using System.Security.Claims; + +namespace EnvelopeGenerator.Server.Services; + +/// +/// Server-side authentication service for envelope receiver access validation. +/// Uses HttpContext to check JWT claims and envelope key authorization. +/// +public class EnvelopeAuthService : IEnvelopeAuthService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + + public EnvelopeAuthService( + IHttpContextAccessor httpContextAccessor, + ILogger logger) + { + _httpContextAccessor = httpContextAccessor; + _logger = logger; + } + + /// + public bool IsAuthenticated(string envelopeKey) + { + if (string.IsNullOrWhiteSpace(envelopeKey)) + { + _logger.LogWarning("IsAuthenticated called with null or empty envelope key"); + return false; + } + + var context = _httpContextAccessor.HttpContext; + + // Check if user is authenticated + if (context?.User?.Identity?.IsAuthenticated != true) + { + _logger.LogDebug("User is not authenticated for envelope {EnvelopeKey}", envelopeKey); + return false; + } + + // Get envelope key from claims + var sub = GetEnvelopeKeyFromClaims(context.User); + + // Verify envelope key matches + var isValid = sub == envelopeKey; + + if (!isValid) + { + _logger.LogWarning( + "Envelope key mismatch: Expected {ExpectedKey}, Got {ActualKey}", + envelopeKey, + sub ?? "(null)"); + } + else + { + _logger.LogDebug("User authenticated for envelope {EnvelopeKey}", envelopeKey); + } + + return isValid; + } + + /// + public string? GetAuthenticatedEnvelopeKey() + { + var context = _httpContextAccessor.HttpContext; + + if (context?.User?.Identity?.IsAuthenticated != true) + return null; + + return GetEnvelopeKeyFromClaims(context.User); + } + + /// + public ClaimsPrincipal? GetCurrentUser() + { + return _httpContextAccessor.HttpContext?.User; + } + + private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user) + { + // Try NameIdentifier first (standard claim) + var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + // Fallback to "sub" claim (JWT standard) + if (string.IsNullOrWhiteSpace(sub)) + { + sub = user.FindFirst("sub")?.Value; + } + + return sub; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs new file mode 100644 index 00000000..181135c4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs @@ -0,0 +1,93 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using DigitalData.Auth.Claims; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Server.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; + +namespace EnvelopeGenerator.Server.Services; + +/// +/// Authorizes receiver access for interactive server pages without calling a controller endpoint. +/// +public class EnvelopeReceiverAuthorizationService( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + IOptions authTokenKeyOptions, + IOptionsMonitor jwtBearerOptionsMonitor, + ILogger logger) +{ + private readonly AuthTokenKeys _authTokenKeys = authTokenKeyOptions.Value; + + /// + /// Returns the authenticated receiver principal for the specified envelope key when authorization succeeds. + /// + public async Task AuthorizeAsync(string envelopeKey, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(envelopeKey)) + return null; + + var httpContext = httpContextAccessor.HttpContext; + if (httpContext is null) + return null; + + if (await IsAuthorizedReceiverAsync(httpContext.User, envelopeKey, cancellationToken)) + return httpContext.User; + + var cookieName = CookieNames.GetEnvelopeReceiverCookieName(_authTokenKeys.Cookie, envelopeKey); + if (!httpContext.Request.Cookies.TryGetValue(cookieName, out var token) || string.IsNullOrWhiteSpace(token)) + { + logger.LogDebug("Receiver cookie '{CookieName}' was not found for envelope '{EnvelopeKey}'.", cookieName, envelopeKey); + return null; + } + + var principal = ValidateReceiverToken(token); + if (principal is null) + return null; + + if (!await IsAuthorizedReceiverAsync(principal, envelopeKey, cancellationToken)) + return null; + + httpContext.User = principal; + + return principal; + } + + /// + /// Checks whether the current request is authorized for the specified envelope key. + /// + public async Task IsAuthorizedAsync(string envelopeKey, CancellationToken cancellationToken = default) + => await AuthorizeAsync(envelopeKey, cancellationToken) is not null; + + private async Task IsAuthorizedReceiverAsync(ClaimsPrincipal? principal, string envelopeKey, CancellationToken cancellationToken) + { + if (principal?.Identity?.IsAuthenticated != true) + return false; + + var authorizationResult = await authorizationService.AuthorizeAsync(principal, AuthPolicy.Receiver); + if (!authorizationResult.Succeeded) + return false; + + var subject = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? principal.FindFirst("sub")?.Value; + + return string.Equals(subject, envelopeKey, StringComparison.Ordinal); + } + + private ClaimsPrincipal? ValidateReceiverToken(string token) + { + try + { + var tokenValidationParameters = jwtBearerOptionsMonitor.Get(AuthScheme.Receiver).TokenValidationParameters.Clone(); + var tokenHandler = new JwtSecurityTokenHandler(); + return tokenHandler.ValidateToken(token, tokenValidationParameters, out _); + } + catch (Exception ex) + { + logger.LogDebug(ex, "Receiver token validation failed."); + return null; + } + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs new file mode 100644 index 00000000..a367a54e --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs @@ -0,0 +1,126 @@ +using System.Security.Claims; +using System.Text.Json; +using EnvelopeGenerator.Application.Common.Dto; +using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; +using EnvelopeGenerator.Application.Documents.Queries; +using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; +using EnvelopeGenerator.Application.Receivers.Queries; +using EnvelopeGenerator.Server.Client.Models; +using EnvelopeGenerator.Server.Client.Models.Constants; +using EnvelopeGenerator.Server.Extensions; +using EnvelopeGenerator.Server.Options; +using MediatR; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto; + +namespace EnvelopeGenerator.Server.Services; + +/// +/// Loads receiver page data directly from MediatR and distributed cache. +/// +public class EnvelopeReceiverPageDataService( + IMediator mediator, + IDistributedCache cache, + IOptions cacheOptions, + IMemoryCache memoryCache) +{ + private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:"; + + /// + /// Loads the PDF document bytes for the authenticated receiver. + /// + public async Task GetDocumentAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + { + var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken); + return document.ByteData; + } + + /// + /// Loads the current receiver's signature placeholders. + /// + public async Task> GetSignaturesAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + { + var receiverId = user.ReceiverId(); + var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken); + + if (document.Elements is not IEnumerable elements) + return []; + + var signatures = elements + .Where(element => element.ReceiverId == receiverId) + .Select(MapSignature) + .ToList(); + + return signatures.Convert(UnitOfLength.Point); + } + + /// + /// Loads the envelope receiver data for the specified envelope key. + /// + public async Task GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancellationToken = default) + { + var result = await mediator.Send(new ReadEnvelopeReceiverQuery { Key = envelopeKey }, cancellationToken); + return result.SingleOrDefault(); + } + + /// + /// Loads the cached signature for the authenticated receiver. + /// + public async Task GetCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + { + var json = await cache.GetStringAsync(GetSignatureCacheKey(user), cancellationToken); + return json is null ? null : JsonSerializer.Deserialize(json); + } + + /// + /// Saves the cached signature for the authenticated receiver. + /// + public async Task SaveCachedSignatureAsync(ClaimsPrincipal user, SignatureCaptureDto signature, CancellationToken cancellationToken = default) + { + var json = JsonSerializer.Serialize(signature); + var options = cacheOptions.Value.SignatureCacheExpiration.HasValue + ? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value } + : new DistributedCacheEntryOptions(); + + await cache.SetStringAsync(GetSignatureCacheKey(user), json, options, cancellationToken); + } + + /// + /// Deletes the cached signature for the authenticated receiver. + /// + public Task DeleteCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default) + => cache.RemoveAsync(GetSignatureCacheKey(user), cancellationToken); + + private static string GetSignatureCacheKey(ClaimsPrincipal user) + => $"{SignatureCacheKeyPrefix}{user.ReceiverSignature()}"; + + private static SignatureDto MapSignature(DocReceiverElementDto element) => new() + { + Id = element.Id, + X = element.X, + Y = element.Y, + Page = element.Page, + SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType + }; + + private static readonly string ReceiverEmailSearchCacheKey = Guid.NewGuid().ToString(); + + public async Task> SearchReceiverEMailsAsync(string emailSearchTerm, CancellationToken cancellationToken = default) + { + + return await memoryCache.GetOrCreateAsync(ReceiverEmailSearchCacheKey + emailSearchTerm, async entry => + { + var query = new ReadReceiverQuery { EmailAddressSearch = emailSearchTerm }; + var receivers = await mediator.Send(query, cancellationToken); + + if(receivers.Any()) + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30); + else + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10); + + return receivers.Select(r => r.EmailAddress); + }) ?? []; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs new file mode 100644 index 00000000..cafd90ad --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs @@ -0,0 +1,29 @@ +using System.Security.Claims; + +namespace EnvelopeGenerator.Server.Services; + +/// +/// Service for handling envelope-specific authentication in SSR (Server-Side Rendering) context. +/// +public interface IEnvelopeAuthService +{ + /// + /// Checks if the current user is authenticated for the given envelope key. + /// Validates both that the user is authenticated AND that the envelope key matches their claims. + /// + /// The envelope key to validate against user claims. + /// True if user is authenticated and envelope key matches; otherwise false. + bool IsAuthenticated(string envelopeKey); + + /// + /// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim). + /// + /// The envelope key if user is authenticated; otherwise null. + string? GetAuthenticatedEnvelopeKey(); + + /// + /// Gets the current HttpContext user principal. + /// + /// ClaimsPrincipal if available; otherwise null. + ClaimsPrincipal? GetCurrentUser(); +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/PdfSharpFontResolver.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/PdfSharpFontResolver.cs new file mode 100644 index 00000000..84acfe22 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/PdfSharpFontResolver.cs @@ -0,0 +1,46 @@ +using PdfSharp.Fonts; + +namespace EnvelopeGenerator.Server.Services; + +/// +/// PdfSharp 6.x IFontResolver for .NET 8. +/// PdfSharp cannot access system fonts on .NET Core/8 without an explicit resolver. +/// This implementation reads fonts directly from the Windows Fonts folder. +/// Register once at startup: GlobalFontSettings.FontResolver = PdfSharpFontResolver.Instance; +/// +public class PdfSharpFontResolver : IFontResolver +{ + public static readonly PdfSharpFontResolver Instance = new(); + + private static readonly string FontsFolder = + Environment.GetFolderPath(Environment.SpecialFolder.Fonts); + + public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic) + { + var key = familyName.ToLowerInvariant() switch + { + "arial" => isBold ? "arialbd" : "arial", + _ => null + }; + + return key is null ? null : new FontResolverInfo(key); + } + + public byte[] GetFont(string faceName) + { + var fileName = faceName switch + { + "arialbd" => "arialbd.ttf", + _ => "arial.ttf", + }; + + var path = Path.Combine(FontsFolder, fileName); + + if (!File.Exists(path)) + throw new FileNotFoundException( + $"Font file not found: {path}. " + + "Ensure Arial is installed on the server."); + + return File.ReadAllBytes(path); + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json new file mode 100644 index 00000000..3940dae0 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.Development.json @@ -0,0 +1,18 @@ +{ +"Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } +}, + "AuthClientParams": { + "Url": "http://172.24.12.39:9090/auth-hub", + "PublicKeys": [ + { + "Issuer": "auth.digitaldata.works", + "Audience": "sign-flow.digitaldata.works" + } + ], + "RetryDelay": "00:00:05" + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json new file mode 100644 index 00000000..e763a015 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json @@ -0,0 +1,271 @@ +{ + "UseSwagger": true, + "UseDbMigration": false, + "DiPMode": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AllowedOrigins": [ + "http://localhost:4200", + "http://172.24.12.39:9090", + "https://localhost:8088", + "http://localhost:5131", + "http://localhost:7192" + ], + "ConnectionStrings": { + "Default": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;", + "DbMigrationTest": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM_DATA_MIGR_TEST;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;" + }, + "DirectorySearchOptions": { + "ServerName": "DD-VMP01-DC01", + "Root": "DC=dd-gan,DC=local,DC=digitaldata,DC=works", + "UserCacheExpirationDays": 1, + "CustomSearchFilters": { + "User": "(&(objectClass=user)(sAMAccountName=*))", + "Group": "(&(objectClass=group)(samAccountName=*))" + } + }, + "AuthClientParams": { + "Url": "http://172.24.12.39:9090/auth-hub", + "PublicKeys": [ + { + "Issuer": "auth.digitaldata.works", + "Audience": "sign-flow.digitaldata.works" + } + ], + "RetryDelay": "00:00:05" + }, + "AuthTokenKeys": { + "Cookie": "AuthToken", + "QueryString": "AuthToken", + "Issuer": "auth.digitaldata.works", + "Audience": "sign-flow.digitaldata.works" + }, + "ApiOptions": { + "BaseUrl": "" + }, + "PdfViewerOptions": { + "ThumbnailBaseScale": 0.75, + "ThumbnailEnableHiDPI": true, + "MainCanvasEnableHiDPI": true, + "ZoomStepPercentage": 5 + }, + "PSPDFKitLicenseKey": "SXCtGGY9XA-31OGUXQK-r7c6AkdLGPm2ljuyDr1qu0kkhLvydg-Do-fxpNUF4Rq3fS_xAnZRNFRHbXpE6sQ2BMcCSVTcXVJO6tPviexjpiT-HnrDEySlUERJnnvh-tmeOWprxS6BySPnSILkmaVQtUfOIUS-cUbvvEYHTvQBKbSF8di4XHQFyfv49ihr51axm3NVV3AXwh2EiKL5C5XdqBZ4sQ4O7vXBjM2zvxdPxlxdcNYmiU83uAzw7B83O_jubPzya4CdUHh_YH7Nlp2gP56MeG1Sw2JhMtfG3Rj14Sg4ctaeL9p6AEWca5dDjJ2li5tFIV2fQSsw6A_cowLu0gtMm5i8IfJXeIcQbMC2-0wGv1oe9hZYJvFMdzhTM_FiejM0agemxt3lJyzuyP8zbBSOgp7Si6A85krLWPZptyZBTG7pp7IHboUHfPMxCXqi-zMsqewOJtQBE2mjntU-lPryKnssOpMPfswwQX7QSkJYV5EMqNmEhQX6mEkp2wcqFzMC7bJQew1aO4pOpvChUaMvb1vgRek0HxLag0nwQYX2YrYGh7F_xXJs-8HNwJe8H0-eW4x4faayCgM5rB5772CCCsD9ThZcvXFrjNHHLGJ8WuBUFm6LArvSfFQdii_7j-_sqHMpeKZt26NFgivj1A==", + "Content-Security-Policy": [ // The first format parameter {0} will be replaced by the nonce value. + "default-src 'self'", + "script-src 'self' 'nonce-{0}' 'unsafe-eval'", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com:*", + "img-src 'self' data: https: blob:", + "font-src 'self' https://fonts.gstatic.com:*", + "connect-src 'self' https://nominatim.openstreetmap.org:* http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* blob:", + "frame-src 'self'", + "media-src 'self'", + "object-src 'self'" + ], + "NLog": { + "throwConfigExceptions": true, + "variables": { + "logDirectory": "E:\\LogFiles\\Digital Data\\signFlow", + "logFileNamePrefix": "${shortdate}-ECM.EnvelopeGenerator.Server" + }, + "targets": { + "infoLogs": { + "type": "File", + "fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log", + "maxArchiveDays": 30 + }, + "errorLogs": { + "type": "File", + "fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log", + "maxArchiveDays": 30 + }, + "criticalLogs": { + "type": "File", + "fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log", + "maxArchiveDays": 30 + } + }, + // Trace, Debug, Info, Warn, Error and *Fatal* + "rules": [ + { + "logger": "*", + "minLevel": "Info", + "maxLevel": "Warn", + "writeTo": "infoLogs" + }, + { + "logger": "*", + "level": "Error", + "writeTo": "errorLogs" + }, + { + "logger": "*", + "level": "Fatal", + "writeTo": "criticalLogs" + } + ] + }, + "ContactLink": { + "Label": "Kontakt", + "Href": "https://digitaldata.works/", + "HrefLang": "de", + "Target": "_blank", + "Title": "Digital Data GmbH" + }, + /* Resx naming format is -> Resource.language.resx (eg: Resource.de_DE.resx). + To add a new language, first you should write the required resx file. + first is the default culture name. */ + "Cultures": [ + { + "Language": "de-DE", + "FIClass": "fi-de" + }, + { + "Language": "en-US", + "FIClass": "fi-us" + } + ], + "DisableMultiLanguage": false, + "Regexes": [ + { + "Pattern": "/^\\p{L}+(?:([\\ \\-\\']|(\\.\\ ))\\p{L}+)*$/u", + "Name": "City", + "Platforms": [ ".NET" ] + }, + { + "Pattern": "/^[a-zA-Z\\u0080-\\u024F]+(?:([\\ \\-\\']|(\\.\\ ))[a-zA-Z\\u0080-\\u024F]+)*$/", + "Name": "City", + "Platforms": [ "javascript" ] + } + ], + "CustomImages": { + "App": { + "Src": "/img/DD_signFLOW_LOGO.png", + "Classes": { + "Main": "signFlow-logo" + } + }, + "Company": { + "Src": "/img/digital_data.svg", + "Classes": { + "Show": "dd-show-logo", + "Locked": "dd-locked-logo" + } + } + }, + "DispatcherParams": { + "SendingProfile": 1, + "AddedWho": "DDEnvelopGenerator", + "ReminderTypeId": 202377, + "EmailAttmt1": "" + }, + "MailParams": { + "Placeholders": { + "[NAME_PORTAL]": "signFlow", + "[SIGNATURE_TYPE]": "signieren", + "[REASON]": "" + } + }, + "GtxMessagingParams": { + "Uri": "https://rest.gtx-messaging.net", + "Path": "smsc/sendsms/f566f7e5-bdf2-4a9a-bf52-ed88215a432e/json", + "Headers": {}, + "QueryParams": { + "from": "signFlow" + } + }, + "TFARegParams": { + "TimeLimit": "00:30:00" + }, + "DbTriggerParams": { + "Envelope": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "EnvelopeHistory": [ "TBSIG_ENVELOPE_HISTORY_AFT_INS" ], + "EmailOut": [ "TBEMLP_EMAIL_OUT_AFT_INS", "TBEMLP_EMAIL_OUT_AFT_UPD" ], + "EnvelopeReceiverReadOnly": [ "TBSIG_ENVELOPE_RECEIVER_READ_ONLY_UPD" ], + "Receiver": [], + "EmailTemplate": [ "TBSIG_EMAIL_TEMPLATE_AFT_UPD" ] + }, + "Cache": { + "SignatureCacheExpiration": null, + "SqlServer": { + "ConnectionString": null, + "SchemaName": "dbo", + "TableName": "TBDD_CACHE" + } + }, + "MainPageTitle": null, + "AnnotationParams": { + "Background": { + "Margin": 0.20, + "BackgroundColor": { + "R": 222, + "G": 220, + "B": 215 + }, + "BorderColor": { + "R": 204, + "G": 202, + "B": 198 + }, + "BorderStyle": "underline", + "BorderWidth": 4 + }, + "DefaultAnnotation": { + "Width": 1, + "Height": 0.5, + "MarginTop": 1 + }, + "Annotations": [ + { + "Name": "Signature", + "MarginTop": 0 + }, + { + "Name": "PositionLabel", + "VerBoundAnnotName": "Signature", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": 0.22 + }, + { + "Name": "Position", + "VerBoundAnnotName": "PositionLabel", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": -0.05 + }, + { + "Name": "CityLabel", + "VerBoundAnnotName": "Position", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": 0.05 + }, + { + "Name": "City", + "VerBoundAnnotName": "CityLabel", + "WidthRatio": 1.2, + "HeightRatio": 0.5, + "MarginTopRatio": -0.05 + }, + { + "Name": "DateLabel", + "VerBoundAnnotName": "City", + "WidthRatio": 1.55, + "HeightRatio": 0.5, + "MarginTopRatio": 0.05 + }, + { + "Name": "Date", + "VerBoundAnnotName": "DateLabel", + "WidthRatio": 1.55, + "HeightRatio": 0.5, + "MarginTopRatio": -0.1 + } + ] + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/dotnet-tools.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/dotnet-tools.json new file mode 100644 index 00000000..807729e4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.9", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/appsettings.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/appsettings.json new file mode 100644 index 00000000..23b77e53 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/appsettings.json @@ -0,0 +1,18 @@ +{ + "Api": { + "BaseUrl": "", + "UsePredefinedReports": false + }, + "PdfViewer": { + "ThumbnailBaseScale": 0.75, + "ThumbnailEnableHiDPI": true, + "ThumbnailMaxDPR": 2.0, + "MainCanvasEnableHiDPI": true, + "MainCanvasMaxDPR": 2.0, + "EnableSmoothZoom": true, + "ZoomTransitionDuration": 900, + "RenderingOpacity": 0.85, + "ThumbnailRenderDelay": 50, + "ZoomStepPercentage": 5 + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/app.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/app.css new file mode 100644 index 00000000..562b15b3 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/app.css @@ -0,0 +1,368 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + padding: 0; + margin: 0; + width: 100%; +} + +html, body { + height: 100%; + overflow: hidden; +} + +main, .page { + margin: 0; + padding: 0; + width: 100%; +} + +article { + height: calc(100vh - 36px); + display: flex; + flex-direction: column; + overflow-y: auto; + padding: 0 !important; + margin: 0 !important; +} + +.receiver-page-layout { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.receiver-signature-panel { + flex: 0 0 auto; +} + +.receiver-viewer-wrapper { + flex: 1 1 0; + min-height: 0; + overflow: hidden; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.dx-blazor-reporting-container { + height: calc(100vh - 166px) !important; + width: 100% !important; +} + +/* ── Force DevExpress viewer pages into a single centered column ─────────── */ +.dxbrv-report-preview-content { + display: flex !important; + flex-direction: column !important; + align-items: center !important; +} + +/* ── Annotation signature checkbox overlays ─────────────────────────────── */ +.annot-sig-cb-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + font-family: "Segoe UI", Arial, sans-serif; + padding: 0 12px; + overflow: hidden; + cursor: pointer; + user-select: none; + background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); + border: none; + color: #ffffff; + box-shadow: 0 2px 8px rgba(44, 62, 80, 0.35); + transition: box-shadow 0.18s, filter 0.18s; +} + +.annot-sig-cb-wrapper:hover { + filter: brightness(1.12); + box-shadow: 0 4px 14px rgba(44, 62, 80, 0.45); +} + +.annot-sig-cb-wrapper--checked { + background: linear-gradient(135deg, #1a6b2a 0%, #27ae60 100%); + box-shadow: 0 2px 8px rgba(26, 107, 42, 0.35); +} + +.annot-sig-cb-wrapper--checked:hover { + filter: brightness(1.1); + box-shadow: 0 4px 14px rgba(26, 107, 42, 0.45); +} + +.annot-sig-cb { + display: none; +} + +.annot-sig-cb__label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; + letter-spacing: 0.01em; +} + +/* ── Envelope info header ────────────────────────────────────────────────── */ +.receiver-info-header { + border-bottom: 1px solid rgba(0,0,0,.08); +} + +.receiver-info-header__gradient { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + padding: 10px 16px 8px; + background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); + color: #fff; +} + +.receiver-info-header__left { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.receiver-info-header__icon { + flex-shrink: 0; + opacity: .85; +} + +.receiver-info-header__title { + font-size: 0.92rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 340px; +} + +.receiver-info-header__sender { + font-size: 0.72rem; + opacity: .8; + margin-top: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 400px; +} + +.receiver-info-header__badges { + display: flex; + flex-wrap: wrap; + gap: 5px; + align-items: center; +} + +.receiver-info-badge { + display: inline-flex; + align-items: center; + background: rgba(255,255,255,.18); + color: #fff; + border-radius: 20px; + padding: 2px 9px; + font-size: 0.70rem; + font-weight: 500; + white-space: nowrap; +} + +.receiver-info-badge--muted { + background: rgba(255,255,255,.10); + opacity: .8; +} + +.receiver-info-badge--accent { + background: rgba(39,174,96,.35); + border: 1px solid rgba(39,174,96,.5); +} + +.receiver-info-message { + padding: 7px 16px; + font-size: 0.78rem; + color: #444; + border-bottom: 1px solid rgba(0,0,0,.05); + white-space: pre-wrap; + line-height: 1.45; +} + +.receiver-info-private-message { + display: flex; + align-items: flex-start; + gap: 4px; + padding: 5px 16px 6px; + font-size: 0.75rem; + color: #5a5a72; + background: #f8f7ff; + border-top: 1px solid rgba(90,80,180,.12); +} + +/* ── Signature action bar ────────────────────────────────────────────────── */ +.receiver-action-bar { + border-bottom: 1px solid rgba(0,0,0,.08); + background: #fff; +} + +.receiver-action-bar__inner { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + padding: 7px 14px; +} + +.receiver-action-bar__progress { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; +} + +/* ── Home page ───────────────────────────────────────────────────────────── */ +.home-page-wrapper { + display: flex; + flex-direction: column; + align-items: center; + min-height: calc(100vh - 36px); + background: linear-gradient(160deg, #1e2e3e 0%, #2c3e50 40%, #3498db 100%); + padding-bottom: 36px; +} + +.home-hero-header { + width: 100%; + padding: 48px 24px 36px; + text-align: center; +} + +.home-hero-header__inner { + display: inline-flex; + align-items: center; + gap: 20px; + color: #fff; +} + +.home-hero-header__icon { + opacity: 0.90; + flex-shrink: 0; +} + +.home-hero-header__title { + font-size: 2rem; + font-weight: 700; + margin: 0; + color: #fff; + letter-spacing: 0.02em; +} + +.home-hero-header__subtitle { + font-size: 0.92rem; + margin: 4px 0 0; + color: rgba(255,255,255,0.75); +} + +.home-content { + width: 100%; + max-width: 520px; + padding: 0 16px; +} + +.home-card { + border-radius: 12px !important; + overflow: hidden; +} + +.home-btn-primary { + background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); + border: none; + color: #fff !important; + font-weight: 700; + border-radius: 8px; + transition: filter 0.15s, box-shadow 0.15s; + box-shadow: 0 2px 10px rgba(44,62,80,0.30); +} + +.home-btn-primary:hover { + filter: brightness(1.12); + box-shadow: 0 4px 16px rgba(44,62,80,0.40); + color: #fff; +} + +.home-feature-badge { + display: inline-flex; + align-items: center; + background: rgba(52,152,219,0.10); + color: #3498db; + border: 1px solid rgba(52,152,219,0.25); + border-radius: 20px; + padding: 4px 12px; + font-size: 0.75rem; + font-weight: 500; +} + +/* ── Footer ──────────────────────────────────────────────────────────────── */ +.receiver-footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 0 20px; + background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); + color: rgba(255, 255, 255, 0.75); + font-size: 0.72rem; + z-index: 100; + flex-shrink: 0; +} + +.receiver-footer a { + color: rgba(255, 255, 255, 0.90); + text-decoration: none; + transition: color 0.15s; +} + +.receiver-footer a:hover { + color: #ffffff; + text-decoration: underline; +} + +.receiver-footer__sep { + opacity: 0.4; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/bootstrap/bootstrap.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/bootstrap/bootstrap.css new file mode 100644 index 00000000..c7c4fbc6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/bootstrap/bootstrap.css @@ -0,0 +1,12063 @@ +@charset "UTF-8"; +/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +:root, +[data-bs-theme=light] { + --bs-blue: #0d6efd; + --bs-indigo: #6610f2; + --bs-purple: #6f42c1; + --bs-pink: #d63384; + --bs-red: #dc3545; + --bs-orange: #fd7e14; + --bs-yellow: #ffc107; + --bs-green: #198754; + --bs-teal: #20c997; + --bs-cyan: #0dcaf0; + --bs-black: #000; + --bs-white: #fff; + --bs-gray: #6c757d; + --bs-gray-dark: #343a40; + --bs-gray-100: #f8f9fa; + --bs-gray-200: #e9ecef; + --bs-gray-300: #dee2e6; + --bs-gray-400: #ced4da; + --bs-gray-500: #adb5bd; + --bs-gray-600: #6c757d; + --bs-gray-700: #495057; + --bs-gray-800: #343a40; + --bs-gray-900: #212529; + --bs-primary: #0d6efd; + --bs-secondary: #6c757d; + --bs-success: #198754; + --bs-info: #0dcaf0; + --bs-warning: #ffc107; + --bs-danger: #dc3545; + --bs-light: #f8f9fa; + --bs-dark: #212529; + --bs-primary-rgb: 13, 110, 253; + --bs-secondary-rgb: 108, 117, 125; + --bs-success-rgb: 25, 135, 84; + --bs-info-rgb: 13, 202, 240; + --bs-warning-rgb: 255, 193, 7; + --bs-danger-rgb: 220, 53, 69; + --bs-light-rgb: 248, 249, 250; + --bs-dark-rgb: 33, 37, 41; + --bs-primary-text-emphasis: #052c65; + --bs-secondary-text-emphasis: #2b2f32; + --bs-success-text-emphasis: #0a3622; + --bs-info-text-emphasis: #055160; + --bs-warning-text-emphasis: #664d03; + --bs-danger-text-emphasis: #58151c; + --bs-light-text-emphasis: #495057; + --bs-dark-text-emphasis: #495057; + --bs-primary-bg-subtle: #cfe2ff; + --bs-secondary-bg-subtle: #e2e3e5; + --bs-success-bg-subtle: #d1e7dd; + --bs-info-bg-subtle: #cff4fc; + --bs-warning-bg-subtle: #fff3cd; + --bs-danger-bg-subtle: #f8d7da; + --bs-light-bg-subtle: #fcfcfd; + --bs-dark-bg-subtle: #ced4da; + --bs-primary-border-subtle: #9ec5fe; + --bs-secondary-border-subtle: #c4c8cb; + --bs-success-border-subtle: #a3cfbb; + --bs-info-border-subtle: #9eeaf9; + --bs-warning-border-subtle: #ffe69c; + --bs-danger-border-subtle: #f1aeb5; + --bs-light-border-subtle: #e9ecef; + --bs-dark-border-subtle: #adb5bd; + --bs-white-rgb: 255, 255, 255; + --bs-black-rgb: 0, 0, 0; + --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); + --bs-body-font-family: var(--bs-font-sans-serif); + --bs-body-font-size: 1rem; + --bs-body-font-weight: 400; + --bs-body-line-height: 1.5; + --bs-body-color: #212529; + --bs-body-color-rgb: 33, 37, 41; + --bs-body-bg: #fff; + --bs-body-bg-rgb: 255, 255, 255; + --bs-emphasis-color: #000; + --bs-emphasis-color-rgb: 0, 0, 0; + --bs-secondary-color: rgba(33, 37, 41, 0.75); + --bs-secondary-color-rgb: 33, 37, 41; + --bs-secondary-bg: #e9ecef; + --bs-secondary-bg-rgb: 233, 236, 239; + --bs-tertiary-color: rgba(33, 37, 41, 0.5); + --bs-tertiary-color-rgb: 33, 37, 41; + --bs-tertiary-bg: #f8f9fa; + --bs-tertiary-bg-rgb: 248, 249, 250; + --bs-heading-color: inherit; + --bs-link-color: #0d6efd; + --bs-link-color-rgb: 13, 110, 253; + --bs-link-decoration: underline; + --bs-link-hover-color: #0a58ca; + --bs-link-hover-color-rgb: 10, 88, 202; + --bs-code-color: #d63384; + --bs-highlight-bg: #fff3cd; + --bs-border-width: 1px; + --bs-border-style: solid; + --bs-border-color: #dee2e6; + --bs-border-color-translucent: rgba(0, 0, 0, 0.175); + --bs-border-radius: 0.375rem; + --bs-border-radius-sm: 0.25rem; + --bs-border-radius-lg: 0.5rem; + --bs-border-radius-xl: 1rem; + --bs-border-radius-xxl: 2rem; + --bs-border-radius-2xl: var(--bs-border-radius-xxl); + --bs-border-radius-pill: 50rem; + --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); + --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); + --bs-focus-ring-width: 0.25rem; + --bs-focus-ring-opacity: 0.25; + --bs-focus-ring-color: rgba(13, 110, 253, 0.25); + --bs-form-valid-color: #198754; + --bs-form-valid-border-color: #198754; + --bs-form-invalid-color: #dc3545; + --bs-form-invalid-border-color: #dc3545; +} + +[data-bs-theme=dark] { + color-scheme: dark; + --bs-body-color: #dee2e6; + --bs-body-color-rgb: 222, 226, 230; + --bs-body-bg: #212529; + --bs-body-bg-rgb: 33, 37, 41; + --bs-emphasis-color: #fff; + --bs-emphasis-color-rgb: 255, 255, 255; + --bs-secondary-color: rgba(222, 226, 230, 0.75); + --bs-secondary-color-rgb: 222, 226, 230; + --bs-secondary-bg: #343a40; + --bs-secondary-bg-rgb: 52, 58, 64; + --bs-tertiary-color: rgba(222, 226, 230, 0.5); + --bs-tertiary-color-rgb: 222, 226, 230; + --bs-tertiary-bg: #2b3035; + --bs-tertiary-bg-rgb: 43, 48, 53; + --bs-primary-text-emphasis: #6ea8fe; + --bs-secondary-text-emphasis: #a7acb1; + --bs-success-text-emphasis: #75b798; + --bs-info-text-emphasis: #6edff6; + --bs-warning-text-emphasis: #ffda6a; + --bs-danger-text-emphasis: #ea868f; + --bs-light-text-emphasis: #f8f9fa; + --bs-dark-text-emphasis: #dee2e6; + --bs-primary-bg-subtle: #031633; + --bs-secondary-bg-subtle: #161719; + --bs-success-bg-subtle: #051b11; + --bs-info-bg-subtle: #032830; + --bs-warning-bg-subtle: #332701; + --bs-danger-bg-subtle: #2c0b0e; + --bs-light-bg-subtle: #343a40; + --bs-dark-bg-subtle: #1a1d20; + --bs-primary-border-subtle: #084298; + --bs-secondary-border-subtle: #41464b; + --bs-success-border-subtle: #0f5132; + --bs-info-border-subtle: #087990; + --bs-warning-border-subtle: #997404; + --bs-danger-border-subtle: #842029; + --bs-light-border-subtle: #495057; + --bs-dark-border-subtle: #343a40; + --bs-heading-color: inherit; + --bs-link-color: #6ea8fe; + --bs-link-hover-color: #8bb9fe; + --bs-link-color-rgb: 110, 168, 254; + --bs-link-hover-color-rgb: 139, 185, 254; + --bs-code-color: #e685b5; + --bs-border-color: #495057; + --bs-border-color-translucent: rgba(255, 255, 255, 0.15); + --bs-form-valid-color: #75b798; + --bs-form-valid-border-color: #75b798; + --bs-form-invalid-color: #ea868f; + --bs-form-invalid-border-color: #ea868f; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +@media (prefers-reduced-motion: no-preference) { + :root { + scroll-behavior: smooth; + } +} + +body { + margin: 0; + font-family: var(--bs-body-font-family); + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + color: var(--bs-body-color); + text-align: var(--bs-body-text-align); + background-color: var(--bs-body-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +hr { + margin: 1rem 0; + color: inherit; + border: 0; + border-top: var(--bs-border-width) solid; + opacity: 0.25; +} + +h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 { + margin-top: 0; + margin-bottom: 0.5rem; + font-weight: 500; + line-height: 1.2; + color: var(--bs-heading-color); +} + +h1, .h1 { + font-size: calc(1.375rem + 1.5vw); +} +@media (min-width: 1200px) { + h1, .h1 { + font-size: 2.5rem; + } +} + +h2, .h2 { + font-size: calc(1.325rem + 0.9vw); +} +@media (min-width: 1200px) { + h2, .h2 { + font-size: 2rem; + } +} + +h3, .h3 { + font-size: calc(1.3rem + 0.6vw); +} +@media (min-width: 1200px) { + h3, .h3 { + font-size: 1.75rem; + } +} + +h4, .h4 { + font-size: calc(1.275rem + 0.3vw); +} +@media (min-width: 1200px) { + h4, .h4 { + font-size: 1.5rem; + } +} + +h5, .h5 { + font-size: 1.25rem; +} + +h6, .h6 { + font-size: 1rem; +} + +p { + margin-top: 0; + margin-bottom: 1rem; +} + +abbr[title] { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + -webkit-text-decoration-skip-ink: none; + text-decoration-skip-ink: none; +} + +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ol, +ul { + padding-left: 2rem; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 1rem; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; + margin-left: 0; +} + +blockquote { + margin: 0 0 1rem; +} + +b, +strong { + font-weight: bolder; +} + +small, .small { + font-size: 0.875em; +} + +mark, .mark { + padding: 0.1875em; + background-color: var(--bs-highlight-bg); +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +a { + color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); + text-decoration: underline; +} +a:hover { + --bs-link-color-rgb: var(--bs-link-hover-color-rgb); +} + +a:not([href]):not([class]), a:not([href]):not([class]):hover { + color: inherit; + text-decoration: none; +} + +pre, +code, +kbd, +samp { + font-family: var(--bs-font-monospace); + font-size: 1em; +} + +pre { + display: block; + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + font-size: 0.875em; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} + +code { + font-size: 0.875em; + color: var(--bs-code-color); + word-wrap: break-word; +} +a > code { + color: inherit; +} + +kbd { + padding: 0.1875rem 0.375rem; + font-size: 0.875em; + color: var(--bs-body-bg); + background-color: var(--bs-body-color); + border-radius: 0.25rem; +} +kbd kbd { + padding: 0; + font-size: 1em; +} + +figure { + margin: 0 0 1rem; +} + +img, +svg { + vertical-align: middle; +} + +table { + caption-side: bottom; + border-collapse: collapse; +} + +caption { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: var(--bs-secondary-color); + text-align: left; +} + +th { + text-align: inherit; + text-align: -webkit-match-parent; +} + +thead, +tbody, +tfoot, +tr, +td, +th { + border-color: inherit; + border-style: solid; + border-width: 0; +} + +label { + display: inline-block; +} + +button { + border-radius: 0; +} + +button:focus:not(:focus-visible) { + outline: 0; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +select { + text-transform: none; +} + +[role=button] { + cursor: pointer; +} + +select { + word-wrap: normal; +} +select:disabled { + opacity: 1; +} + +[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { + display: none !important; +} + +button, +[type=button], +[type=reset], +[type=submit] { + -webkit-appearance: button; +} +button:not(:disabled), +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled) { + cursor: pointer; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +textarea { + resize: vertical; +} + +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + float: left; + width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: calc(1.275rem + 0.3vw); + line-height: inherit; +} +@media (min-width: 1200px) { + legend { + font-size: 1.5rem; + } +} +legend + * { + clear: left; +} + +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; +} + +::-webkit-inner-spin-button { + height: auto; +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +/* rtl:raw: +[type="tel"], +[type="url"], +[type="email"], +[type="number"] { + direction: ltr; +} +*/ +::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-color-swatch-wrapper { + padding: 0; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +::file-selector-button { + font: inherit; + -webkit-appearance: button; +} + +output { + display: inline-block; +} + +iframe { + border: 0; +} + +summary { + display: list-item; + cursor: pointer; +} + +progress { + vertical-align: baseline; +} + +[hidden] { + display: none !important; +} + +.lead { + font-size: 1.25rem; + font-weight: 300; +} + +.display-1 { + font-size: calc(1.625rem + 4.5vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-1 { + font-size: 5rem; + } +} + +.display-2 { + font-size: calc(1.575rem + 3.9vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-2 { + font-size: 4.5rem; + } +} + +.display-3 { + font-size: calc(1.525rem + 3.3vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-3 { + font-size: 4rem; + } +} + +.display-4 { + font-size: calc(1.475rem + 2.7vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-4 { + font-size: 3.5rem; + } +} + +.display-5 { + font-size: calc(1.425rem + 2.1vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-5 { + font-size: 3rem; + } +} + +.display-6 { + font-size: calc(1.375rem + 1.5vw); + font-weight: 300; + line-height: 1.2; +} +@media (min-width: 1200px) { + .display-6 { + font-size: 2.5rem; + } +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline-item { + display: inline-block; +} +.list-inline-item:not(:last-child) { + margin-right: 0.5rem; +} + +.initialism { + font-size: 0.875em; + text-transform: uppercase; +} + +.blockquote { + margin-bottom: 1rem; + font-size: 1.25rem; +} +.blockquote > :last-child { + margin-bottom: 0; +} + +.blockquote-footer { + margin-top: -1rem; + margin-bottom: 1rem; + font-size: 0.875em; + color: #6c757d; +} +.blockquote-footer::before { + content: "— "; +} + +.img-fluid { + max-width: 100%; + height: auto; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: var(--bs-body-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + max-width: 100%; + height: auto; +} + +.figure { + display: inline-block; +} + +.figure-img { + margin-bottom: 0.5rem; + line-height: 1; +} + +.figure-caption { + font-size: 0.875em; + color: var(--bs-secondary-color); +} + +.container, +.container-fluid, +.container-xxl, +.container-xl, +.container-lg, +.container-md, +.container-sm { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container-sm, .container { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container-md, .container-sm, .container { + max-width: 720px; + } +} +@media (min-width: 992px) { + .container-lg, .container-md, .container-sm, .container { + max-width: 960px; + } +} +@media (min-width: 1200px) { + .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1140px; + } +} +@media (min-width: 1400px) { + .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { + max-width: 1320px; + } +} +:root { + --bs-breakpoint-xs: 0; + --bs-breakpoint-sm: 576px; + --bs-breakpoint-md: 768px; + --bs-breakpoint-lg: 992px; + --bs-breakpoint-xl: 1200px; + --bs-breakpoint-xxl: 1400px; +} + +.row { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; + display: flex; + flex-wrap: wrap; + margin-top: calc(-1 * var(--bs-gutter-y)); + margin-right: calc(-0.5 * var(--bs-gutter-x)); + margin-left: calc(-0.5 * var(--bs-gutter-x)); +} +.row > * { + flex-shrink: 0; + width: 100%; + max-width: 100%; + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); + margin-top: var(--bs-gutter-y); +} + +.col { + flex: 1 0 0%; +} + +.row-cols-auto > * { + flex: 0 0 auto; + width: auto; +} + +.row-cols-1 > * { + flex: 0 0 auto; + width: 100%; +} + +.row-cols-2 > * { + flex: 0 0 auto; + width: 50%; +} + +.row-cols-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; +} + +.row-cols-4 > * { + flex: 0 0 auto; + width: 25%; +} + +.row-cols-5 > * { + flex: 0 0 auto; + width: 20%; +} + +.row-cols-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; +} + +.col-1 { + flex: 0 0 auto; + width: 8.33333333%; +} + +.col-2 { + flex: 0 0 auto; + width: 16.66666667%; +} + +.col-3 { + flex: 0 0 auto; + width: 25%; +} + +.col-4 { + flex: 0 0 auto; + width: 33.33333333%; +} + +.col-5 { + flex: 0 0 auto; + width: 41.66666667%; +} + +.col-6 { + flex: 0 0 auto; + width: 50%; +} + +.col-7 { + flex: 0 0 auto; + width: 58.33333333%; +} + +.col-8 { + flex: 0 0 auto; + width: 66.66666667%; +} + +.col-9 { + flex: 0 0 auto; + width: 75%; +} + +.col-10 { + flex: 0 0 auto; + width: 83.33333333%; +} + +.col-11 { + flex: 0 0 auto; + width: 91.66666667%; +} + +.col-12 { + flex: 0 0 auto; + width: 100%; +} + +.offset-1 { + margin-left: 8.33333333%; +} + +.offset-2 { + margin-left: 16.66666667%; +} + +.offset-3 { + margin-left: 25%; +} + +.offset-4 { + margin-left: 33.33333333%; +} + +.offset-5 { + margin-left: 41.66666667%; +} + +.offset-6 { + margin-left: 50%; +} + +.offset-7 { + margin-left: 58.33333333%; +} + +.offset-8 { + margin-left: 66.66666667%; +} + +.offset-9 { + margin-left: 75%; +} + +.offset-10 { + margin-left: 83.33333333%; +} + +.offset-11 { + margin-left: 91.66666667%; +} + +.g-0, +.gx-0 { + --bs-gutter-x: 0; +} + +.g-0, +.gy-0 { + --bs-gutter-y: 0; +} + +.g-1, +.gx-1 { + --bs-gutter-x: 0.25rem; +} + +.g-1, +.gy-1 { + --bs-gutter-y: 0.25rem; +} + +.g-2, +.gx-2 { + --bs-gutter-x: 0.5rem; +} + +.g-2, +.gy-2 { + --bs-gutter-y: 0.5rem; +} + +.g-3, +.gx-3 { + --bs-gutter-x: 1rem; +} + +.g-3, +.gy-3 { + --bs-gutter-y: 1rem; +} + +.g-4, +.gx-4 { + --bs-gutter-x: 1.5rem; +} + +.g-4, +.gy-4 { + --bs-gutter-y: 1.5rem; +} + +.g-5, +.gx-5 { + --bs-gutter-x: 3rem; +} + +.g-5, +.gy-5 { + --bs-gutter-y: 3rem; +} + +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0%; + } + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + .col-sm-auto { + flex: 0 0 auto; + width: auto; + } + .col-sm-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-sm-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-sm-3 { + flex: 0 0 auto; + width: 25%; + } + .col-sm-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-sm-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-sm-6 { + flex: 0 0 auto; + width: 50%; + } + .col-sm-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-sm-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-sm-9 { + flex: 0 0 auto; + width: 75%; + } + .col-sm-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-sm-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-sm-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-sm-0 { + margin-left: 0; + } + .offset-sm-1 { + margin-left: 8.33333333%; + } + .offset-sm-2 { + margin-left: 16.66666667%; + } + .offset-sm-3 { + margin-left: 25%; + } + .offset-sm-4 { + margin-left: 33.33333333%; + } + .offset-sm-5 { + margin-left: 41.66666667%; + } + .offset-sm-6 { + margin-left: 50%; + } + .offset-sm-7 { + margin-left: 58.33333333%; + } + .offset-sm-8 { + margin-left: 66.66666667%; + } + .offset-sm-9 { + margin-left: 75%; + } + .offset-sm-10 { + margin-left: 83.33333333%; + } + .offset-sm-11 { + margin-left: 91.66666667%; + } + .g-sm-0, + .gx-sm-0 { + --bs-gutter-x: 0; + } + .g-sm-0, + .gy-sm-0 { + --bs-gutter-y: 0; + } + .g-sm-1, + .gx-sm-1 { + --bs-gutter-x: 0.25rem; + } + .g-sm-1, + .gy-sm-1 { + --bs-gutter-y: 0.25rem; + } + .g-sm-2, + .gx-sm-2 { + --bs-gutter-x: 0.5rem; + } + .g-sm-2, + .gy-sm-2 { + --bs-gutter-y: 0.5rem; + } + .g-sm-3, + .gx-sm-3 { + --bs-gutter-x: 1rem; + } + .g-sm-3, + .gy-sm-3 { + --bs-gutter-y: 1rem; + } + .g-sm-4, + .gx-sm-4 { + --bs-gutter-x: 1.5rem; + } + .g-sm-4, + .gy-sm-4 { + --bs-gutter-y: 1.5rem; + } + .g-sm-5, + .gx-sm-5 { + --bs-gutter-x: 3rem; + } + .g-sm-5, + .gy-sm-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0%; + } + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + .col-md-auto { + flex: 0 0 auto; + width: auto; + } + .col-md-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-md-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-md-3 { + flex: 0 0 auto; + width: 25%; + } + .col-md-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-md-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-md-6 { + flex: 0 0 auto; + width: 50%; + } + .col-md-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-md-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-md-9 { + flex: 0 0 auto; + width: 75%; + } + .col-md-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-md-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-md-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-md-0 { + margin-left: 0; + } + .offset-md-1 { + margin-left: 8.33333333%; + } + .offset-md-2 { + margin-left: 16.66666667%; + } + .offset-md-3 { + margin-left: 25%; + } + .offset-md-4 { + margin-left: 33.33333333%; + } + .offset-md-5 { + margin-left: 41.66666667%; + } + .offset-md-6 { + margin-left: 50%; + } + .offset-md-7 { + margin-left: 58.33333333%; + } + .offset-md-8 { + margin-left: 66.66666667%; + } + .offset-md-9 { + margin-left: 75%; + } + .offset-md-10 { + margin-left: 83.33333333%; + } + .offset-md-11 { + margin-left: 91.66666667%; + } + .g-md-0, + .gx-md-0 { + --bs-gutter-x: 0; + } + .g-md-0, + .gy-md-0 { + --bs-gutter-y: 0; + } + .g-md-1, + .gx-md-1 { + --bs-gutter-x: 0.25rem; + } + .g-md-1, + .gy-md-1 { + --bs-gutter-y: 0.25rem; + } + .g-md-2, + .gx-md-2 { + --bs-gutter-x: 0.5rem; + } + .g-md-2, + .gy-md-2 { + --bs-gutter-y: 0.5rem; + } + .g-md-3, + .gx-md-3 { + --bs-gutter-x: 1rem; + } + .g-md-3, + .gy-md-3 { + --bs-gutter-y: 1rem; + } + .g-md-4, + .gx-md-4 { + --bs-gutter-x: 1.5rem; + } + .g-md-4, + .gy-md-4 { + --bs-gutter-y: 1.5rem; + } + .g-md-5, + .gx-md-5 { + --bs-gutter-x: 3rem; + } + .g-md-5, + .gy-md-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0%; + } + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + .col-lg-auto { + flex: 0 0 auto; + width: auto; + } + .col-lg-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-lg-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-lg-3 { + flex: 0 0 auto; + width: 25%; + } + .col-lg-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-lg-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-lg-6 { + flex: 0 0 auto; + width: 50%; + } + .col-lg-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-lg-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-lg-9 { + flex: 0 0 auto; + width: 75%; + } + .col-lg-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-lg-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-lg-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-lg-0 { + margin-left: 0; + } + .offset-lg-1 { + margin-left: 8.33333333%; + } + .offset-lg-2 { + margin-left: 16.66666667%; + } + .offset-lg-3 { + margin-left: 25%; + } + .offset-lg-4 { + margin-left: 33.33333333%; + } + .offset-lg-5 { + margin-left: 41.66666667%; + } + .offset-lg-6 { + margin-left: 50%; + } + .offset-lg-7 { + margin-left: 58.33333333%; + } + .offset-lg-8 { + margin-left: 66.66666667%; + } + .offset-lg-9 { + margin-left: 75%; + } + .offset-lg-10 { + margin-left: 83.33333333%; + } + .offset-lg-11 { + margin-left: 91.66666667%; + } + .g-lg-0, + .gx-lg-0 { + --bs-gutter-x: 0; + } + .g-lg-0, + .gy-lg-0 { + --bs-gutter-y: 0; + } + .g-lg-1, + .gx-lg-1 { + --bs-gutter-x: 0.25rem; + } + .g-lg-1, + .gy-lg-1 { + --bs-gutter-y: 0.25rem; + } + .g-lg-2, + .gx-lg-2 { + --bs-gutter-x: 0.5rem; + } + .g-lg-2, + .gy-lg-2 { + --bs-gutter-y: 0.5rem; + } + .g-lg-3, + .gx-lg-3 { + --bs-gutter-x: 1rem; + } + .g-lg-3, + .gy-lg-3 { + --bs-gutter-y: 1rem; + } + .g-lg-4, + .gx-lg-4 { + --bs-gutter-x: 1.5rem; + } + .g-lg-4, + .gy-lg-4 { + --bs-gutter-y: 1.5rem; + } + .g-lg-5, + .gx-lg-5 { + --bs-gutter-x: 3rem; + } + .g-lg-5, + .gy-lg-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0%; + } + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + .col-xl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xl-0 { + margin-left: 0; + } + .offset-xl-1 { + margin-left: 8.33333333%; + } + .offset-xl-2 { + margin-left: 16.66666667%; + } + .offset-xl-3 { + margin-left: 25%; + } + .offset-xl-4 { + margin-left: 33.33333333%; + } + .offset-xl-5 { + margin-left: 41.66666667%; + } + .offset-xl-6 { + margin-left: 50%; + } + .offset-xl-7 { + margin-left: 58.33333333%; + } + .offset-xl-8 { + margin-left: 66.66666667%; + } + .offset-xl-9 { + margin-left: 75%; + } + .offset-xl-10 { + margin-left: 83.33333333%; + } + .offset-xl-11 { + margin-left: 91.66666667%; + } + .g-xl-0, + .gx-xl-0 { + --bs-gutter-x: 0; + } + .g-xl-0, + .gy-xl-0 { + --bs-gutter-y: 0; + } + .g-xl-1, + .gx-xl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xl-1, + .gy-xl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xl-2, + .gx-xl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xl-2, + .gy-xl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xl-3, + .gx-xl-3 { + --bs-gutter-x: 1rem; + } + .g-xl-3, + .gy-xl-3 { + --bs-gutter-y: 1rem; + } + .g-xl-4, + .gx-xl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xl-4, + .gy-xl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xl-5, + .gx-xl-5 { + --bs-gutter-x: 3rem; + } + .g-xl-5, + .gy-xl-5 { + --bs-gutter-y: 3rem; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0%; + } + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } + .col-xxl-auto { + flex: 0 0 auto; + width: auto; + } + .col-xxl-1 { + flex: 0 0 auto; + width: 8.33333333%; + } + .col-xxl-2 { + flex: 0 0 auto; + width: 16.66666667%; + } + .col-xxl-3 { + flex: 0 0 auto; + width: 25%; + } + .col-xxl-4 { + flex: 0 0 auto; + width: 33.33333333%; + } + .col-xxl-5 { + flex: 0 0 auto; + width: 41.66666667%; + } + .col-xxl-6 { + flex: 0 0 auto; + width: 50%; + } + .col-xxl-7 { + flex: 0 0 auto; + width: 58.33333333%; + } + .col-xxl-8 { + flex: 0 0 auto; + width: 66.66666667%; + } + .col-xxl-9 { + flex: 0 0 auto; + width: 75%; + } + .col-xxl-10 { + flex: 0 0 auto; + width: 83.33333333%; + } + .col-xxl-11 { + flex: 0 0 auto; + width: 91.66666667%; + } + .col-xxl-12 { + flex: 0 0 auto; + width: 100%; + } + .offset-xxl-0 { + margin-left: 0; + } + .offset-xxl-1 { + margin-left: 8.33333333%; + } + .offset-xxl-2 { + margin-left: 16.66666667%; + } + .offset-xxl-3 { + margin-left: 25%; + } + .offset-xxl-4 { + margin-left: 33.33333333%; + } + .offset-xxl-5 { + margin-left: 41.66666667%; + } + .offset-xxl-6 { + margin-left: 50%; + } + .offset-xxl-7 { + margin-left: 58.33333333%; + } + .offset-xxl-8 { + margin-left: 66.66666667%; + } + .offset-xxl-9 { + margin-left: 75%; + } + .offset-xxl-10 { + margin-left: 83.33333333%; + } + .offset-xxl-11 { + margin-left: 91.66666667%; + } + .g-xxl-0, + .gx-xxl-0 { + --bs-gutter-x: 0; + } + .g-xxl-0, + .gy-xxl-0 { + --bs-gutter-y: 0; + } + .g-xxl-1, + .gx-xxl-1 { + --bs-gutter-x: 0.25rem; + } + .g-xxl-1, + .gy-xxl-1 { + --bs-gutter-y: 0.25rem; + } + .g-xxl-2, + .gx-xxl-2 { + --bs-gutter-x: 0.5rem; + } + .g-xxl-2, + .gy-xxl-2 { + --bs-gutter-y: 0.5rem; + } + .g-xxl-3, + .gx-xxl-3 { + --bs-gutter-x: 1rem; + } + .g-xxl-3, + .gy-xxl-3 { + --bs-gutter-y: 1rem; + } + .g-xxl-4, + .gx-xxl-4 { + --bs-gutter-x: 1.5rem; + } + .g-xxl-4, + .gy-xxl-4 { + --bs-gutter-y: 1.5rem; + } + .g-xxl-5, + .gx-xxl-5 { + --bs-gutter-x: 3rem; + } + .g-xxl-5, + .gy-xxl-5 { + --bs-gutter-y: 3rem; + } +} +.table { + --bs-table-color-type: initial; + --bs-table-bg-type: initial; + --bs-table-color-state: initial; + --bs-table-bg-state: initial; + --bs-table-color: var(--bs-body-color); + --bs-table-bg: var(--bs-body-bg); + --bs-table-border-color: var(--bs-border-color); + --bs-table-accent-bg: transparent; + --bs-table-striped-color: var(--bs-body-color); + --bs-table-striped-bg: rgba(0, 0, 0, 0.05); + --bs-table-active-color: var(--bs-body-color); + --bs-table-active-bg: rgba(0, 0, 0, 0.1); + --bs-table-hover-color: var(--bs-body-color); + --bs-table-hover-bg: rgba(0, 0, 0, 0.075); + width: 100%; + margin-bottom: 1rem; + vertical-align: top; + border-color: var(--bs-table-border-color); +} +.table > :not(caption) > * > * { + padding: 0.5rem 0.5rem; + color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color))); + background-color: var(--bs-table-bg); + border-bottom-width: var(--bs-border-width); + box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); +} +.table > tbody { + vertical-align: inherit; +} +.table > thead { + vertical-align: bottom; +} + +.table-group-divider { + border-top: calc(var(--bs-border-width) * 2) solid currentcolor; +} + +.caption-top { + caption-side: top; +} + +.table-sm > :not(caption) > * > * { + padding: 0.25rem 0.25rem; +} + +.table-bordered > :not(caption) > * { + border-width: var(--bs-border-width) 0; +} +.table-bordered > :not(caption) > * > * { + border-width: 0 var(--bs-border-width); +} + +.table-borderless > :not(caption) > * > * { + border-bottom-width: 0; +} +.table-borderless > :not(:first-child) { + border-top-width: 0; +} + +.table-striped > tbody > tr:nth-of-type(odd) > * { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); +} + +.table-striped-columns > :not(caption) > tr > :nth-child(even) { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); +} + +.table-active { + --bs-table-color-state: var(--bs-table-active-color); + --bs-table-bg-state: var(--bs-table-active-bg); +} + +.table-hover > tbody > tr:hover > * { + --bs-table-color-state: var(--bs-table-hover-color); + --bs-table-bg-state: var(--bs-table-hover-bg); +} + +.table-primary { + --bs-table-color: #000; + --bs-table-bg: #cfe2ff; + --bs-table-border-color: #bacbe6; + --bs-table-striped-bg: #c5d7f2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bacbe6; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfd1ec; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-secondary { + --bs-table-color: #000; + --bs-table-bg: #e2e3e5; + --bs-table-border-color: #cbccce; + --bs-table-striped-bg: #d7d8da; + --bs-table-striped-color: #000; + --bs-table-active-bg: #cbccce; + --bs-table-active-color: #000; + --bs-table-hover-bg: #d1d2d4; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-success { + --bs-table-color: #000; + --bs-table-bg: #d1e7dd; + --bs-table-border-color: #bcd0c7; + --bs-table-striped-bg: #c7dbd2; + --bs-table-striped-color: #000; + --bs-table-active-bg: #bcd0c7; + --bs-table-active-color: #000; + --bs-table-hover-bg: #c1d6cc; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-info { + --bs-table-color: #000; + --bs-table-bg: #cff4fc; + --bs-table-border-color: #badce3; + --bs-table-striped-bg: #c5e8ef; + --bs-table-striped-color: #000; + --bs-table-active-bg: #badce3; + --bs-table-active-color: #000; + --bs-table-hover-bg: #bfe2e9; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-warning { + --bs-table-color: #000; + --bs-table-bg: #fff3cd; + --bs-table-border-color: #e6dbb9; + --bs-table-striped-bg: #f2e7c3; + --bs-table-striped-color: #000; + --bs-table-active-bg: #e6dbb9; + --bs-table-active-color: #000; + --bs-table-hover-bg: #ece1be; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-danger { + --bs-table-color: #000; + --bs-table-bg: #f8d7da; + --bs-table-border-color: #dfc2c4; + --bs-table-striped-bg: #eccccf; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfc2c4; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5c7ca; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-light { + --bs-table-color: #000; + --bs-table-bg: #f8f9fa; + --bs-table-border-color: #dfe0e1; + --bs-table-striped-bg: #ecedee; + --bs-table-striped-color: #000; + --bs-table-active-bg: #dfe0e1; + --bs-table-active-color: #000; + --bs-table-hover-bg: #e5e6e7; + --bs-table-hover-color: #000; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-dark { + --bs-table-color: #fff; + --bs-table-bg: #212529; + --bs-table-border-color: #373b3e; + --bs-table-striped-bg: #2c3034; + --bs-table-striped-color: #fff; + --bs-table-active-bg: #373b3e; + --bs-table-active-color: #fff; + --bs-table-hover-bg: #323539; + --bs-table-hover-color: #fff; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); +} + +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +@media (max-width: 575.98px) { + .table-responsive-sm { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 767.98px) { + .table-responsive-md { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 991.98px) { + .table-responsive-lg { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1199.98px) { + .table-responsive-xl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +@media (max-width: 1399.98px) { + .table-responsive-xxl { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} +.form-label { + margin-bottom: 0.5rem; +} + +.col-form-label { + padding-top: calc(0.375rem + var(--bs-border-width)); + padding-bottom: calc(0.375rem + var(--bs-border-width)); + margin-bottom: 0; + font-size: inherit; + line-height: 1.5; +} + +.col-form-label-lg { + padding-top: calc(0.5rem + var(--bs-border-width)); + padding-bottom: calc(0.5rem + var(--bs-border-width)); + font-size: 1.25rem; +} + +.col-form-label-sm { + padding-top: calc(0.25rem + var(--bs-border-width)); + padding-bottom: calc(0.25rem + var(--bs-border-width)); + font-size: 0.875rem; +} + +.form-text { + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-secondary-color); +} + +.form-control { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--bs-body-bg); + background-clip: padding-box; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control { + transition: none; + } +} +.form-control[type=file] { + overflow: hidden; +} +.form-control[type=file]:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control:focus { + color: var(--bs-body-color); + background-color: var(--bs-body-bg); + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-control::-webkit-date-and-time-value { + min-width: 85px; + height: 1.5em; + margin: 0; +} +.form-control::-webkit-datetime-edit { + display: block; + padding: 0; +} +.form-control::-moz-placeholder { + color: var(--bs-secondary-color); + opacity: 1; +} +.form-control::placeholder { + color: var(--bs-secondary-color); + opacity: 1; +} +.form-control:disabled { + background-color: var(--bs-secondary-bg); + opacity: 1; +} +.form-control::-webkit-file-upload-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: var(--bs-border-width); + border-radius: 0; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +.form-control::file-selector-button { + padding: 0.375rem 0.75rem; + margin: -0.375rem -0.75rem; + -webkit-margin-end: 0.75rem; + margin-inline-end: 0.75rem; + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); + pointer-events: none; + border-color: inherit; + border-style: solid; + border-width: 0; + border-inline-end-width: var(--bs-border-width); + border-radius: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-control::-webkit-file-upload-button { + -webkit-transition: none; + transition: none; + } + .form-control::file-selector-button { + transition: none; + } +} +.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { + background-color: var(--bs-secondary-bg); +} +.form-control:hover:not(:disabled):not([readonly])::file-selector-button { + background-color: var(--bs-secondary-bg); +} + +.form-control-plaintext { + display: block; + width: 100%; + padding: 0.375rem 0; + margin-bottom: 0; + line-height: 1.5; + color: var(--bs-body-color); + background-color: transparent; + border: solid transparent; + border-width: var(--bs-border-width) 0; +} +.form-control-plaintext:focus { + outline: 0; +} +.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { + padding-right: 0; + padding-left: 0; +} + +.form-control-sm { + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} +.form-control-sm::-webkit-file-upload-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; +} +.form-control-sm::file-selector-button { + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; + -webkit-margin-end: 0.5rem; + margin-inline-end: 0.5rem; +} + +.form-control-lg { + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} +.form-control-lg::-webkit-file-upload-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; +} +.form-control-lg::file-selector-button { + padding: 0.5rem 1rem; + margin: -0.5rem -1rem; + -webkit-margin-end: 1rem; + margin-inline-end: 1rem; +} + +textarea.form-control { + min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); +} +textarea.form-control-sm { + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); +} +textarea.form-control-lg { + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); +} + +.form-control-color { + width: 3rem; + height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); + padding: 0.375rem; +} +.form-control-color:not(:disabled):not([readonly]) { + cursor: pointer; +} +.form-control-color::-moz-color-swatch { + border: 0 !important; + border-radius: var(--bs-border-radius); +} +.form-control-color::-webkit-color-swatch { + border: 0 !important; + border-radius: var(--bs-border-radius); +} +.form-control-color.form-control-sm { + height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); +} +.form-control-color.form-control-lg { + height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); +} + +.form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + display: block; + width: 100%; + padding: 0.375rem 2.25rem 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--bs-body-bg); + background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-select { + transition: none; + } +} +.form-select:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-select[multiple], .form-select[size]:not([size="1"]) { + padding-right: 0.75rem; + background-image: none; +} +.form-select:disabled { + background-color: var(--bs-secondary-bg); +} +.form-select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 var(--bs-body-color); +} + +.form-select-sm { + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} + +.form-select-lg { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} + +[data-bs-theme=dark] .form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); +} + +.form-check { + display: block; + min-height: 1.5rem; + padding-left: 1.5em; + margin-bottom: 0.125rem; +} +.form-check .form-check-input { + float: left; + margin-left: -1.5em; +} + +.form-check-reverse { + padding-right: 1.5em; + padding-left: 0; + text-align: right; +} +.form-check-reverse .form-check-input { + float: right; + margin-right: -1.5em; + margin-left: 0; +} + +.form-check-input { + --bs-form-check-bg: var(--bs-body-bg); + width: 1em; + height: 1em; + margin-top: 0.25em; + vertical-align: top; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--bs-form-check-bg); + background-image: var(--bs-form-check-bg-image); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + border: var(--bs-border-width) solid var(--bs-border-color); + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; +} +.form-check-input[type=checkbox] { + border-radius: 0.25em; +} +.form-check-input[type=radio] { + border-radius: 50%; +} +.form-check-input:active { + filter: brightness(90%); +} +.form-check-input:focus { + border-color: #86b7fe; + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-check-input:checked { + background-color: #0d6efd; + border-color: #0d6efd; +} +.form-check-input:checked[type=checkbox] { + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e"); +} +.form-check-input:checked[type=radio] { + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-check-input[type=checkbox]:indeterminate { + background-color: #0d6efd; + border-color: #0d6efd; + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); +} +.form-check-input:disabled { + pointer-events: none; + filter: none; + opacity: 0.5; +} +.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + cursor: default; + opacity: 0.5; +} + +.form-switch { + padding-left: 2.5em; +} +.form-switch .form-check-input { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); + width: 2em; + margin-left: -2.5em; + background-image: var(--bs-form-switch-bg); + background-position: left center; + border-radius: 2em; + transition: background-position 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-switch .form-check-input { + transition: none; + } +} +.form-switch .form-check-input:focus { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e"); +} +.form-switch .form-check-input:checked { + background-position: right center; + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-switch.form-check-reverse { + padding-right: 2.5em; + padding-left: 0; +} +.form-switch.form-check-reverse .form-check-input { + margin-right: -2.5em; + margin-left: 0; +} + +.form-check-inline { + display: inline-block; + margin-right: 1rem; +} + +.btn-check { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.btn-check[disabled] + .btn, .btn-check:disabled + .btn { + pointer-events: none; + filter: none; + opacity: 0.65; +} + +[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e"); +} + +.form-range { + width: 100%; + height: 1.5rem; + padding: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: transparent; +} +.form-range:focus { + outline: 0; +} +.form-range:focus::-webkit-slider-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range:focus::-moz-range-thumb { + box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.form-range::-moz-focus-outer { + border: 0; +} +.form-range::-webkit-slider-thumb { + width: 1rem; + height: 1rem; + margin-top: -0.25rem; + -webkit-appearance: none; + appearance: none; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-webkit-slider-thumb { + -webkit-transition: none; + transition: none; + } +} +.form-range::-webkit-slider-thumb:active { + background-color: #b6d4fe; +} +.form-range::-webkit-slider-runnable-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: var(--bs-tertiary-bg); + border-color: transparent; + border-radius: 1rem; +} +.form-range::-moz-range-thumb { + width: 1rem; + height: 1rem; + -moz-appearance: none; + appearance: none; + background-color: #0d6efd; + border: 0; + border-radius: 1rem; + -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-range::-moz-range-thumb { + -moz-transition: none; + transition: none; + } +} +.form-range::-moz-range-thumb:active { + background-color: #b6d4fe; +} +.form-range::-moz-range-track { + width: 100%; + height: 0.5rem; + color: transparent; + cursor: pointer; + background-color: var(--bs-tertiary-bg); + border-color: transparent; + border-radius: 1rem; +} +.form-range:disabled { + pointer-events: none; +} +.form-range:disabled::-webkit-slider-thumb { + background-color: var(--bs-secondary-color); +} +.form-range:disabled::-moz-range-thumb { + background-color: var(--bs-secondary-color); +} + +.form-floating { + position: relative; +} +.form-floating > .form-control, +.form-floating > .form-control-plaintext, +.form-floating > .form-select { + height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + min-height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + line-height: 1.25; +} +.form-floating > label { + position: absolute; + top: 0; + left: 0; + z-index: 2; + height: 100%; + padding: 1rem 0.75rem; + overflow: hidden; + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + border: var(--bs-border-width) solid transparent; + transform-origin: 0 0; + transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .form-floating > label { + transition: none; + } +} +.form-floating > .form-control, +.form-floating > .form-control-plaintext { + padding: 1rem 0.75rem; +} +.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder { + color: transparent; +} +.form-floating > .form-control::placeholder, +.form-floating > .form-control-plaintext::placeholder { + color: transparent; +} +.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown), +.form-floating > .form-control-plaintext:focus, +.form-floating > .form-control-plaintext:not(:placeholder-shown) { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:-webkit-autofill, +.form-floating > .form-control-plaintext:-webkit-autofill { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-select { + padding-top: 1.625rem; + padding-bottom: 0.625rem; +} +.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label { + color: rgba(var(--bs-body-color-rgb), 0.65); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label, +.form-floating > .form-control-plaintext ~ label, +.form-floating > .form-select ~ label { + color: rgba(var(--bs-body-color-rgb), 0.65); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after { + position: absolute; + inset: 1rem 0.375rem; + z-index: -1; + height: 1.5em; + content: ""; + background-color: var(--bs-body-bg); + border-radius: var(--bs-border-radius); +} +.form-floating > .form-control:focus ~ label::after, +.form-floating > .form-control:not(:placeholder-shown) ~ label::after, +.form-floating > .form-control-plaintext ~ label::after, +.form-floating > .form-select ~ label::after { + position: absolute; + inset: 1rem 0.375rem; + z-index: -1; + height: 1.5em; + content: ""; + background-color: var(--bs-body-bg); + border-radius: var(--bs-border-radius); +} +.form-floating > .form-control:-webkit-autofill ~ label { + color: rgba(var(--bs-body-color-rgb), 0.65); + transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); +} +.form-floating > .form-control-plaintext ~ label { + border-width: var(--bs-border-width) 0; +} +.form-floating > :disabled ~ label, +.form-floating > .form-control:disabled ~ label { + color: #6c757d; +} +.form-floating > :disabled ~ label::after, +.form-floating > .form-control:disabled ~ label::after { + background-color: var(--bs-secondary-bg); +} + +.input-group { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} +.input-group > .form-control, +.input-group > .form-select, +.input-group > .form-floating { + position: relative; + flex: 1 1 auto; + width: 1%; + min-width: 0; +} +.input-group > .form-control:focus, +.input-group > .form-select:focus, +.input-group > .form-floating:focus-within { + z-index: 5; +} +.input-group .btn { + position: relative; + z-index: 2; +} +.input-group .btn:focus { + z-index: 5; +} + +.input-group-text { + display: flex; + align-items: center; + padding: 0.375rem 0.75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: var(--bs-body-color); + text-align: center; + white-space: nowrap; + background-color: var(--bs-tertiary-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); +} + +.input-group-lg > .form-control, +.input-group-lg > .form-select, +.input-group-lg > .input-group-text, +.input-group-lg > .btn { + padding: 0.5rem 1rem; + font-size: 1.25rem; + border-radius: var(--bs-border-radius-lg); +} + +.input-group-sm > .form-control, +.input-group-sm > .form-select, +.input-group-sm > .input-group-text, +.input-group-sm > .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: var(--bs-border-radius-sm); +} + +.input-group-lg > .form-select, +.input-group-sm > .form-select { + padding-right: 3rem; +} + +.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3), +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control, +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4), +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control, +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { + margin-left: calc(var(--bs-border-width) * -1); + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group > .form-floating:not(:first-child) > .form-control, +.input-group > .form-floating:not(:first-child) > .form-select { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-form-valid-color); +} + +.valid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: var(--bs-success); + border-radius: var(--bs-border-radius); +} + +.was-validated :valid ~ .valid-feedback, +.was-validated :valid ~ .valid-tooltip, +.is-valid ~ .valid-feedback, +.is-valid ~ .valid-tooltip { + display: block; +} + +.was-validated .form-control:valid, .form-control.is-valid { + border-color: var(--bs-form-valid-border-color); + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:valid:focus, .form-control.is-valid:focus { + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} + +.was-validated textarea.form-control:valid, textarea.form-control.is-valid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:valid, .form-select.is-valid { + border-color: var(--bs-form-valid-border-color); +} +.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + padding-right: 4.125rem; + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:valid:focus, .form-select.is-valid:focus { + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} + +.was-validated .form-control-color:valid, .form-control-color.is-valid { + width: calc(3rem + calc(1.5em + 0.75rem)); +} + +.was-validated .form-check-input:valid, .form-check-input.is-valid { + border-color: var(--bs-form-valid-border-color); +} +.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { + background-color: var(--bs-form-valid-color); +} +.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} +.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { + color: var(--bs-form-valid-color); +} + +.form-check-inline .form-check-input ~ .valid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid, +.was-validated .input-group > .form-select:not(:focus):valid, +.input-group > .form-select:not(:focus).is-valid, +.was-validated .input-group > .form-floating:not(:focus-within):valid, +.input-group > .form-floating:not(:focus-within).is-valid { + z-index: 3; +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: var(--bs-form-invalid-color); +} + +.invalid-tooltip { + position: absolute; + top: 100%; + z-index: 5; + display: none; + max-width: 100%; + padding: 0.25rem 0.5rem; + margin-top: 0.1rem; + font-size: 0.875rem; + color: #fff; + background-color: var(--bs-danger); + border-radius: var(--bs-border-radius); +} + +.was-validated :invalid ~ .invalid-feedback, +.was-validated :invalid ~ .invalid-tooltip, +.is-invalid ~ .invalid-feedback, +.is-invalid ~ .invalid-tooltip { + display: block; +} + +.was-validated .form-control:invalid, .form-control.is-invalid { + border-color: var(--bs-form-invalid-border-color); + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} + +.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { + padding-right: calc(1.5em + 0.75rem); + background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); +} + +.was-validated .form-select:invalid, .form-select.is-invalid { + border-color: var(--bs-form-invalid-border-color); +} +.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + padding-right: 4.125rem; + background-position: right 0.75rem center, center right 2.25rem; + background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} +.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} + +.was-validated .form-control-color:invalid, .form-control-color.is-invalid { + width: calc(3rem + calc(1.5em + 0.75rem)); +} + +.was-validated .form-check-input:invalid, .form-check-input.is-invalid { + border-color: var(--bs-form-invalid-border-color); +} +.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { + background-color: var(--bs-form-invalid-color); +} +.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} +.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { + color: var(--bs-form-invalid-color); +} + +.form-check-inline .form-check-input ~ .invalid-feedback { + margin-left: 0.5em; +} + +.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid, +.was-validated .input-group > .form-select:not(:focus):invalid, +.input-group > .form-select:not(:focus).is-invalid, +.was-validated .input-group > .form-floating:not(:focus-within):invalid, +.input-group > .form-floating:not(:focus-within).is-invalid { + z-index: 4; +} + +.btn { + --bs-btn-padding-x: 0.75rem; + --bs-btn-padding-y: 0.375rem; + --bs-btn-font-family: ; + --bs-btn-font-size: 1rem; + --bs-btn-font-weight: 400; + --bs-btn-line-height: 1.5; + --bs-btn-color: var(--bs-body-color); + --bs-btn-bg: transparent; + --bs-btn-border-width: var(--bs-border-width); + --bs-btn-border-color: transparent; + --bs-btn-border-radius: var(--bs-border-radius); + --bs-btn-hover-border-color: transparent; + --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + --bs-btn-disabled-opacity: 0.65; + --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5); + display: inline-block; + padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x); + font-family: var(--bs-btn-font-family); + font-size: var(--bs-btn-font-size); + font-weight: var(--bs-btn-font-weight); + line-height: var(--bs-btn-line-height); + color: var(--bs-btn-color); + text-align: center; + text-decoration: none; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + border: var(--bs-btn-border-width) solid var(--bs-btn-border-color); + border-radius: var(--bs-btn-border-radius); + background-color: var(--bs-btn-bg); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .btn { + transition: none; + } +} +.btn:hover { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); +} +.btn-check + .btn:hover { + color: var(--bs-btn-color); + background-color: var(--bs-btn-bg); + border-color: var(--bs-btn-border-color); +} +.btn:focus-visible { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:focus-visible + .btn { + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show { + color: var(--bs-btn-active-color); + background-color: var(--bs-btn-active-bg); + border-color: var(--bs-btn-active-border-color); +} +.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible { + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn:disabled, .btn.disabled, fieldset:disabled .btn { + color: var(--bs-btn-disabled-color); + pointer-events: none; + background-color: var(--bs-btn-disabled-bg); + border-color: var(--bs-btn-disabled-border-color); + opacity: var(--bs-btn-disabled-opacity); +} + +.btn-primary { + --bs-btn-color: #fff; + --bs-btn-bg: #0d6efd; + --bs-btn-border-color: #0d6efd; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #0b5ed7; + --bs-btn-hover-border-color: #0a58ca; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #0a58ca; + --bs-btn-active-border-color: #0a53be; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #0d6efd; + --bs-btn-disabled-border-color: #0d6efd; +} + +.btn-secondary { + --bs-btn-color: #fff; + --bs-btn-bg: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #5c636a; + --bs-btn-hover-border-color: #565e64; + --bs-btn-focus-shadow-rgb: 130, 138, 145; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #565e64; + --bs-btn-active-border-color: #51585e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #6c757d; + --bs-btn-disabled-border-color: #6c757d; +} + +.btn-success { + --bs-btn-color: #fff; + --bs-btn-bg: #198754; + --bs-btn-border-color: #198754; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #157347; + --bs-btn-hover-border-color: #146c43; + --bs-btn-focus-shadow-rgb: 60, 153, 110; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #146c43; + --bs-btn-active-border-color: #13653f; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #198754; + --bs-btn-disabled-border-color: #198754; +} + +.btn-info { + --bs-btn-color: #000; + --bs-btn-bg: #0dcaf0; + --bs-btn-border-color: #0dcaf0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #31d2f2; + --bs-btn-hover-border-color: #25cff2; + --bs-btn-focus-shadow-rgb: 11, 172, 204; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #3dd5f3; + --bs-btn-active-border-color: #25cff2; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #0dcaf0; + --bs-btn-disabled-border-color: #0dcaf0; +} + +.btn-warning { + --bs-btn-color: #000; + --bs-btn-bg: #ffc107; + --bs-btn-border-color: #ffc107; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #ffca2c; + --bs-btn-hover-border-color: #ffc720; + --bs-btn-focus-shadow-rgb: 217, 164, 6; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #ffcd39; + --bs-btn-active-border-color: #ffc720; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #ffc107; + --bs-btn-disabled-border-color: #ffc107; +} + +.btn-danger { + --bs-btn-color: #fff; + --bs-btn-bg: #dc3545; + --bs-btn-border-color: #dc3545; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #bb2d3b; + --bs-btn-hover-border-color: #b02a37; + --bs-btn-focus-shadow-rgb: 225, 83, 97; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #b02a37; + --bs-btn-active-border-color: #a52834; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #dc3545; + --bs-btn-disabled-border-color: #dc3545; +} + +.btn-light { + --bs-btn-color: #000; + --bs-btn-bg: #f8f9fa; + --bs-btn-border-color: #f8f9fa; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #d3d4d5; + --bs-btn-hover-border-color: #c6c7c8; + --bs-btn-focus-shadow-rgb: 211, 212, 213; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #c6c7c8; + --bs-btn-active-border-color: #babbbc; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #f8f9fa; + --bs-btn-disabled-border-color: #f8f9fa; +} + +.btn-dark { + --bs-btn-color: #fff; + --bs-btn-bg: #212529; + --bs-btn-border-color: #212529; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #424649; + --bs-btn-hover-border-color: #373b3e; + --bs-btn-focus-shadow-rgb: 66, 70, 73; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #4d5154; + --bs-btn-active-border-color: #373b3e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #212529; + --bs-btn-disabled-border-color: #212529; +} + +.btn-outline-primary { + --bs-btn-color: #0d6efd; + --bs-btn-border-color: #0d6efd; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #0d6efd; + --bs-btn-hover-border-color: #0d6efd; + --bs-btn-focus-shadow-rgb: 13, 110, 253; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #0d6efd; + --bs-btn-active-border-color: #0d6efd; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #0d6efd; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #0d6efd; + --bs-gradient: none; +} + +.btn-outline-secondary { + --bs-btn-color: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #6c757d; + --bs-btn-hover-border-color: #6c757d; + --bs-btn-focus-shadow-rgb: 108, 117, 125; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #6c757d; + --bs-btn-active-border-color: #6c757d; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #6c757d; + --bs-gradient: none; +} + +.btn-outline-success { + --bs-btn-color: #198754; + --bs-btn-border-color: #198754; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #198754; + --bs-btn-hover-border-color: #198754; + --bs-btn-focus-shadow-rgb: 25, 135, 84; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #198754; + --bs-btn-active-border-color: #198754; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #198754; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #198754; + --bs-gradient: none; +} + +.btn-outline-info { + --bs-btn-color: #0dcaf0; + --bs-btn-border-color: #0dcaf0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #0dcaf0; + --bs-btn-hover-border-color: #0dcaf0; + --bs-btn-focus-shadow-rgb: 13, 202, 240; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #0dcaf0; + --bs-btn-active-border-color: #0dcaf0; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #0dcaf0; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #0dcaf0; + --bs-gradient: none; +} + +.btn-outline-warning { + --bs-btn-color: #ffc107; + --bs-btn-border-color: #ffc107; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #ffc107; + --bs-btn-hover-border-color: #ffc107; + --bs-btn-focus-shadow-rgb: 255, 193, 7; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #ffc107; + --bs-btn-active-border-color: #ffc107; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #ffc107; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #ffc107; + --bs-gradient: none; +} + +.btn-outline-danger { + --bs-btn-color: #dc3545; + --bs-btn-border-color: #dc3545; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #dc3545; + --bs-btn-hover-border-color: #dc3545; + --bs-btn-focus-shadow-rgb: 220, 53, 69; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #dc3545; + --bs-btn-active-border-color: #dc3545; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #dc3545; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #dc3545; + --bs-gradient: none; +} + +.btn-outline-light { + --bs-btn-color: #f8f9fa; + --bs-btn-border-color: #f8f9fa; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #f8f9fa; + --bs-btn-hover-border-color: #f8f9fa; + --bs-btn-focus-shadow-rgb: 248, 249, 250; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #f8f9fa; + --bs-btn-active-border-color: #f8f9fa; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #f8f9fa; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #f8f9fa; + --bs-gradient: none; +} + +.btn-outline-dark { + --bs-btn-color: #212529; + --bs-btn-border-color: #212529; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #212529; + --bs-btn-hover-border-color: #212529; + --bs-btn-focus-shadow-rgb: 33, 37, 41; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #212529; + --bs-btn-active-border-color: #212529; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #212529; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #212529; + --bs-gradient: none; +} + +.btn-link { + --bs-btn-font-weight: 400; + --bs-btn-color: var(--bs-link-color); + --bs-btn-bg: transparent; + --bs-btn-border-color: transparent; + --bs-btn-hover-color: var(--bs-link-hover-color); + --bs-btn-hover-border-color: transparent; + --bs-btn-active-color: var(--bs-link-hover-color); + --bs-btn-active-border-color: transparent; + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-border-color: transparent; + --bs-btn-box-shadow: 0 0 0 #000; + --bs-btn-focus-shadow-rgb: 49, 132, 253; + text-decoration: underline; +} +.btn-link:focus-visible { + color: var(--bs-btn-color); +} +.btn-link:hover { + color: var(--bs-btn-hover-color); +} + +.btn-lg, .btn-group-lg > .btn { + --bs-btn-padding-y: 0.5rem; + --bs-btn-padding-x: 1rem; + --bs-btn-font-size: 1.25rem; + --bs-btn-border-radius: var(--bs-border-radius-lg); +} + +.btn-sm, .btn-group-sm > .btn { + --bs-btn-padding-y: 0.25rem; + --bs-btn-padding-x: 0.5rem; + --bs-btn-font-size: 0.875rem; + --bs-btn-border-radius: var(--bs-border-radius-sm); +} + +.fade { + transition: opacity 0.15s linear; +} +@media (prefers-reduced-motion: reduce) { + .fade { + transition: none; + } +} +.fade:not(.show) { + opacity: 0; +} + +.collapse:not(.show) { + display: none; +} + +.collapsing { + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing { + transition: none; + } +} +.collapsing.collapse-horizontal { + width: 0; + height: auto; + transition: width 0.35s ease; +} +@media (prefers-reduced-motion: reduce) { + .collapsing.collapse-horizontal { + transition: none; + } +} + +.dropup, +.dropend, +.dropdown, +.dropstart, +.dropup-center, +.dropdown-center { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; +} +.dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} +.dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropdown-menu { + --bs-dropdown-zindex: 1000; + --bs-dropdown-min-width: 10rem; + --bs-dropdown-padding-x: 0; + --bs-dropdown-padding-y: 0.5rem; + --bs-dropdown-spacer: 0.125rem; + --bs-dropdown-font-size: 1rem; + --bs-dropdown-color: var(--bs-body-color); + --bs-dropdown-bg: var(--bs-body-bg); + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-border-radius: var(--bs-border-radius); + --bs-dropdown-border-width: var(--bs-border-width); + --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width)); + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-divider-margin-y: 0.5rem; + --bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-dropdown-link-color: var(--bs-body-color); + --bs-dropdown-link-hover-color: var(--bs-body-color); + --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #0d6efd; + --bs-dropdown-link-disabled-color: var(--bs-tertiary-color); + --bs-dropdown-item-padding-x: 1rem; + --bs-dropdown-item-padding-y: 0.25rem; + --bs-dropdown-header-color: #6c757d; + --bs-dropdown-header-padding-x: 1rem; + --bs-dropdown-header-padding-y: 0.5rem; + position: absolute; + z-index: var(--bs-dropdown-zindex); + display: none; + min-width: var(--bs-dropdown-min-width); + padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); + margin: 0; + font-size: var(--bs-dropdown-font-size); + color: var(--bs-dropdown-color); + text-align: left; + list-style: none; + background-color: var(--bs-dropdown-bg); + background-clip: padding-box; + border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); + border-radius: var(--bs-dropdown-border-radius); +} +.dropdown-menu[data-bs-popper] { + top: 100%; + left: 0; + margin-top: var(--bs-dropdown-spacer); +} + +.dropdown-menu-start { + --bs-position: start; +} +.dropdown-menu-start[data-bs-popper] { + right: auto; + left: 0; +} + +.dropdown-menu-end { + --bs-position: end; +} +.dropdown-menu-end[data-bs-popper] { + right: 0; + left: auto; +} + +@media (min-width: 576px) { + .dropdown-menu-sm-start { + --bs-position: start; + } + .dropdown-menu-sm-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-sm-end { + --bs-position: end; + } + .dropdown-menu-sm-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 768px) { + .dropdown-menu-md-start { + --bs-position: start; + } + .dropdown-menu-md-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-md-end { + --bs-position: end; + } + .dropdown-menu-md-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 992px) { + .dropdown-menu-lg-start { + --bs-position: start; + } + .dropdown-menu-lg-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-lg-end { + --bs-position: end; + } + .dropdown-menu-lg-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 1200px) { + .dropdown-menu-xl-start { + --bs-position: start; + } + .dropdown-menu-xl-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-xl-end { + --bs-position: end; + } + .dropdown-menu-xl-end[data-bs-popper] { + right: 0; + left: auto; + } +} +@media (min-width: 1400px) { + .dropdown-menu-xxl-start { + --bs-position: start; + } + .dropdown-menu-xxl-start[data-bs-popper] { + right: auto; + left: 0; + } + .dropdown-menu-xxl-end { + --bs-position: end; + } + .dropdown-menu-xxl-end[data-bs-popper] { + right: 0; + left: auto; + } +} +.dropup .dropdown-menu[data-bs-popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: var(--bs-dropdown-spacer); +} +.dropup .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0; + border-right: 0.3em solid transparent; + border-bottom: 0.3em solid; + border-left: 0.3em solid transparent; +} +.dropup .dropdown-toggle:empty::after { + margin-left: 0; +} + +.dropend .dropdown-menu[data-bs-popper] { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: var(--bs-dropdown-spacer); +} +.dropend .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0; + border-bottom: 0.3em solid transparent; + border-left: 0.3em solid; +} +.dropend .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropend .dropdown-toggle::after { + vertical-align: 0; +} + +.dropstart .dropdown-menu[data-bs-popper] { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: var(--bs-dropdown-spacer); +} +.dropstart .dropdown-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; +} +.dropstart .dropdown-toggle::after { + display: none; +} +.dropstart .dropdown-toggle::before { + display: inline-block; + margin-right: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid transparent; + border-right: 0.3em solid; + border-bottom: 0.3em solid transparent; +} +.dropstart .dropdown-toggle:empty::after { + margin-left: 0; +} +.dropstart .dropdown-toggle::before { + vertical-align: 0; +} + +.dropdown-divider { + height: 0; + margin: var(--bs-dropdown-divider-margin-y) 0; + overflow: hidden; + border-top: 1px solid var(--bs-dropdown-divider-bg); + opacity: 1; +} + +.dropdown-item { + display: block; + width: 100%; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + clear: both; + font-weight: 400; + color: var(--bs-dropdown-link-color); + text-align: inherit; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border: 0; + border-radius: var(--bs-dropdown-item-border-radius, 0); +} +.dropdown-item:hover, .dropdown-item:focus { + color: var(--bs-dropdown-link-hover-color); + background-color: var(--bs-dropdown-link-hover-bg); +} +.dropdown-item.active, .dropdown-item:active { + color: var(--bs-dropdown-link-active-color); + text-decoration: none; + background-color: var(--bs-dropdown-link-active-bg); +} +.dropdown-item.disabled, .dropdown-item:disabled { + color: var(--bs-dropdown-link-disabled-color); + pointer-events: none; + background-color: transparent; +} + +.dropdown-menu.show { + display: block; +} + +.dropdown-header { + display: block; + padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x); + margin-bottom: 0; + font-size: 0.875rem; + color: var(--bs-dropdown-header-color); + white-space: nowrap; +} + +.dropdown-item-text { + display: block; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + color: var(--bs-dropdown-link-color); +} + +.dropdown-menu-dark { + --bs-dropdown-color: #dee2e6; + --bs-dropdown-bg: #343a40; + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-box-shadow: ; + --bs-dropdown-link-color: #dee2e6; + --bs-dropdown-link-hover-color: #fff; + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #0d6efd; + --bs-dropdown-link-disabled-color: #adb5bd; + --bs-dropdown-header-color: #adb5bd; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + flex: 1 1 auto; +} +.btn-group > .btn-check:checked + .btn, +.btn-group > .btn-check:focus + .btn, +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn-check:checked + .btn, +.btn-group-vertical > .btn-check:focus + .btn, +.btn-group-vertical > .btn:hover, +.btn-group-vertical > .btn:focus, +.btn-group-vertical > .btn:active, +.btn-group-vertical > .btn.active { + z-index: 1; +} + +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} +.btn-toolbar .input-group { + width: auto; +} + +.btn-group { + border-radius: var(--bs-border-radius); +} +.btn-group > :not(.btn-check:first-child) + .btn, +.btn-group > .btn-group:not(:first-child) { + margin-left: calc(var(--bs-border-width) * -1); +} +.btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn.dropdown-toggle-split:first-child, +.btn-group > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:nth-child(n+3), +.btn-group > :not(.btn-check) + .btn, +.btn-group > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.dropdown-toggle-split { + padding-right: 0.5625rem; + padding-left: 0.5625rem; +} +.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after { + margin-left: 0; +} +.dropstart .dropdown-toggle-split::before { + margin-right: 0; +} + +.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { + padding-right: 0.375rem; + padding-left: 0.375rem; +} + +.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { + padding-right: 0.75rem; + padding-left: 0.75rem; +} + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + width: 100%; +} +.btn-group-vertical > .btn:not(:first-child), +.btn-group-vertical > .btn-group:not(:first-child) { + margin-top: calc(var(--bs-border-width) * -1); +} +.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group-vertical > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn ~ .btn, +.btn-group-vertical > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav { + --bs-nav-link-padding-x: 1rem; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-link-color); + --bs-nav-link-hover-color: var(--bs-link-hover-color); + --bs-nav-link-disabled-color: var(--bs-secondary-color); + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav-link { + display: block; + padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x); + font-size: var(--bs-nav-link-font-size); + font-weight: var(--bs-nav-link-font-weight); + color: var(--bs-nav-link-color); + text-decoration: none; + background: none; + border: 0; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .nav-link { + transition: none; + } +} +.nav-link:hover, .nav-link:focus { + color: var(--bs-nav-link-hover-color); +} +.nav-link:focus-visible { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} +.nav-link.disabled, .nav-link:disabled { + color: var(--bs-nav-link-disabled-color); + pointer-events: none; + cursor: default; +} + +.nav-tabs { + --bs-nav-tabs-border-width: var(--bs-border-width); + --bs-nav-tabs-border-color: var(--bs-border-color); + --bs-nav-tabs-border-radius: var(--bs-border-radius); + --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color); + --bs-nav-tabs-link-active-color: var(--bs-emphasis-color); + --bs-nav-tabs-link-active-bg: var(--bs-body-bg); + --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg); + border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color); +} +.nav-tabs .nav-link { + margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width)); + border: var(--bs-nav-tabs-border-width) solid transparent; + border-top-left-radius: var(--bs-nav-tabs-border-radius); + border-top-right-radius: var(--bs-nav-tabs-border-radius); +} +.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { + isolation: isolate; + border-color: var(--bs-nav-tabs-link-hover-border-color); +} +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--bs-nav-tabs-link-active-color); + background-color: var(--bs-nav-tabs-link-active-bg); + border-color: var(--bs-nav-tabs-link-active-border-color); +} +.nav-tabs .dropdown-menu { + margin-top: calc(-1 * var(--bs-nav-tabs-border-width)); + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-pills { + --bs-nav-pills-border-radius: var(--bs-border-radius); + --bs-nav-pills-link-active-color: #fff; + --bs-nav-pills-link-active-bg: #0d6efd; +} +.nav-pills .nav-link { + border-radius: var(--bs-nav-pills-border-radius); +} +.nav-pills .nav-link.active, +.nav-pills .show > .nav-link { + color: var(--bs-nav-pills-link-active-color); + background-color: var(--bs-nav-pills-link-active-bg); +} + +.nav-underline { + --bs-nav-underline-gap: 1rem; + --bs-nav-underline-border-width: 0.125rem; + --bs-nav-underline-link-active-color: var(--bs-emphasis-color); + gap: var(--bs-nav-underline-gap); +} +.nav-underline .nav-link { + padding-right: 0; + padding-left: 0; + border-bottom: var(--bs-nav-underline-border-width) solid transparent; +} +.nav-underline .nav-link:hover, .nav-underline .nav-link:focus { + border-bottom-color: currentcolor; +} +.nav-underline .nav-link.active, +.nav-underline .show > .nav-link { + font-weight: 700; + color: var(--bs-nav-underline-link-active-color); + border-bottom-color: currentcolor; +} + +.nav-fill > .nav-link, +.nav-fill .nav-item { + flex: 1 1 auto; + text-align: center; +} + +.nav-justified > .nav-link, +.nav-justified .nav-item { + flex-basis: 0; + flex-grow: 1; + text-align: center; +} + +.nav-fill .nav-item .nav-link, +.nav-justified .nav-item .nav-link { + width: 100%; +} + +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} + +.navbar { + --bs-navbar-padding-x: 0; + --bs-navbar-padding-y: 0.5rem; + --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65); + --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8); + --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3); + --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-padding-y: 0.3125rem; + --bs-navbar-brand-margin-end: 1rem; + --bs-navbar-brand-font-size: 1.25rem; + --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-nav-link-padding-x: 0.5rem; + --bs-navbar-toggler-padding-y: 0.25rem; + --bs-navbar-toggler-padding-x: 0.75rem; + --bs-navbar-toggler-font-size: 1.25rem; + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); + --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15); + --bs-navbar-toggler-border-radius: var(--bs-border-radius); + --bs-navbar-toggler-focus-width: 0.25rem; + --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out; + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x); +} +.navbar > .container, +.navbar > .container-fluid, +.navbar > .container-sm, +.navbar > .container-md, +.navbar > .container-lg, +.navbar > .container-xl, +.navbar > .container-xxl { + display: flex; + flex-wrap: inherit; + align-items: center; + justify-content: space-between; +} +.navbar-brand { + padding-top: var(--bs-navbar-brand-padding-y); + padding-bottom: var(--bs-navbar-brand-padding-y); + margin-right: var(--bs-navbar-brand-margin-end); + font-size: var(--bs-navbar-brand-font-size); + color: var(--bs-navbar-brand-color); + text-decoration: none; + white-space: nowrap; +} +.navbar-brand:hover, .navbar-brand:focus { + color: var(--bs-navbar-brand-hover-color); +} + +.navbar-nav { + --bs-nav-link-padding-x: 0; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-navbar-color); + --bs-nav-link-hover-color: var(--bs-navbar-hover-color); + --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color); + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .nav-link.active, .navbar-nav .nav-link.show { + color: var(--bs-navbar-active-color); +} +.navbar-nav .dropdown-menu { + position: static; +} + +.navbar-text { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + color: var(--bs-navbar-color); +} +.navbar-text a, +.navbar-text a:hover, +.navbar-text a:focus { + color: var(--bs-navbar-active-color); +} + +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} + +.navbar-toggler { + padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x); + font-size: var(--bs-navbar-toggler-font-size); + line-height: 1; + color: var(--bs-navbar-color); + background-color: transparent; + border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color); + border-radius: var(--bs-navbar-toggler-border-radius); + transition: var(--bs-navbar-toggler-transition); +} +@media (prefers-reduced-motion: reduce) { + .navbar-toggler { + transition: none; + } +} +.navbar-toggler:hover { + text-decoration: none; +} +.navbar-toggler:focus { + text-decoration: none; + outline: 0; + box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width); +} + +.navbar-toggler-icon { + display: inline-block; + width: 1.5em; + height: 1.5em; + vertical-align: middle; + background-image: var(--bs-navbar-toggler-icon-bg); + background-repeat: no-repeat; + background-position: center; + background-size: 100%; +} + +.navbar-nav-scroll { + max-height: var(--bs-scroll-height, 75vh); + overflow-y: auto; +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-sm .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } + .navbar-expand-sm .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-sm .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-sm .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 768px) { + .navbar-expand-md { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-md .navbar-nav { + flex-direction: row; + } + .navbar-expand-md .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-md .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-md .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-md .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-md .navbar-toggler { + display: none; + } + .navbar-expand-md .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-md .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-md .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 992px) { + .navbar-expand-lg { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-lg .navbar-nav { + flex-direction: row; + } + .navbar-expand-lg .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-lg .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-lg .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-lg .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-lg .navbar-toggler { + display: none; + } + .navbar-expand-lg .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-lg .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-lg .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1200px) { + .navbar-expand-xl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xl .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-xl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xl .navbar-toggler { + display: none; + } + .navbar-expand-xl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xl .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-xl .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +@media (min-width: 1400px) { + .navbar-expand-xxl { + flex-wrap: nowrap; + justify-content: flex-start; + } + .navbar-expand-xxl .navbar-nav { + flex-direction: row; + } + .navbar-expand-xxl .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-xxl .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); + } + .navbar-expand-xxl .navbar-nav-scroll { + overflow: visible; + } + .navbar-expand-xxl .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-xxl .navbar-toggler { + display: none; + } + .navbar-expand-xxl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xxl .offcanvas .offcanvas-header { + display: none; + } + .navbar-expand-xxl .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + } +} +.navbar-expand { + flex-wrap: nowrap; + justify-content: flex-start; +} +.navbar-expand .navbar-nav { + flex-direction: row; +} +.navbar-expand .navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-expand .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); +} +.navbar-expand .navbar-nav-scroll { + overflow: visible; +} +.navbar-expand .navbar-collapse { + display: flex !important; + flex-basis: auto; +} +.navbar-expand .navbar-toggler { + display: none; +} +.navbar-expand .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; +} +.navbar-expand .offcanvas .offcanvas-header { + display: none; +} +.navbar-expand .offcanvas .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; +} + +.navbar-dark, +.navbar[data-bs-theme=dark] { + --bs-navbar-color: rgba(255, 255, 255, 0.55); + --bs-navbar-hover-color: rgba(255, 255, 255, 0.75); + --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25); + --bs-navbar-active-color: #fff; + --bs-navbar-brand-color: #fff; + --bs-navbar-brand-hover-color: #fff; + --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1); + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +[data-bs-theme=dark] .navbar-toggler-icon { + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); +} + +.card { + --bs-card-spacer-y: 1rem; + --bs-card-spacer-x: 1rem; + --bs-card-title-spacer-y: 0.5rem; + --bs-card-title-color: ; + --bs-card-subtitle-color: ; + --bs-card-border-width: var(--bs-border-width); + --bs-card-border-color: var(--bs-border-color-translucent); + --bs-card-border-radius: var(--bs-border-radius); + --bs-card-box-shadow: ; + --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-card-cap-padding-y: 0.5rem; + --bs-card-cap-padding-x: 1rem; + --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03); + --bs-card-cap-color: ; + --bs-card-height: ; + --bs-card-color: ; + --bs-card-bg: var(--bs-body-bg); + --bs-card-img-overlay-padding: 1rem; + --bs-card-group-margin: 0.75rem; + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + height: var(--bs-card-height); + color: var(--bs-body-color); + word-wrap: break-word; + background-color: var(--bs-card-bg); + background-clip: border-box; + border: var(--bs-card-border-width) solid var(--bs-card-border-color); + border-radius: var(--bs-card-border-radius); +} +.card > hr { + margin-right: 0; + margin-left: 0; +} +.card > .list-group { + border-top: inherit; + border-bottom: inherit; +} +.card > .list-group:first-child { + border-top-width: 0; + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); +} +.card > .list-group:last-child { + border-bottom-width: 0; + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); +} +.card > .card-header + .list-group, +.card > .list-group + .card-footer { + border-top: 0; +} + +.card-body { + flex: 1 1 auto; + padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x); + color: var(--bs-card-color); +} + +.card-title { + margin-bottom: var(--bs-card-title-spacer-y); + color: var(--bs-card-title-color); +} + +.card-subtitle { + margin-top: calc(-0.5 * var(--bs-card-title-spacer-y)); + margin-bottom: 0; + color: var(--bs-card-subtitle-color); +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link + .card-link { + margin-left: var(--bs-card-spacer-x); +} + +.card-header { + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); + margin-bottom: 0; + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color); +} +.card-header:first-child { + border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0; +} + +.card-footer { + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-top: var(--bs-card-border-width) solid var(--bs-card-border-color); +} +.card-footer:last-child { + border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius); +} + +.card-header-tabs { + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-bottom: calc(-1 * var(--bs-card-cap-padding-y)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); + border-bottom: 0; +} +.card-header-tabs .nav-link.active { + background-color: var(--bs-card-bg); + border-bottom-color: var(--bs-card-bg); +} + +.card-header-pills { + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); +} + +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: var(--bs-card-img-overlay-padding); + border-radius: var(--bs-card-inner-border-radius); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; +} + +.card-img, +.card-img-top { + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); +} + +.card-img, +.card-img-bottom { + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); +} + +.card-group > .card { + margin-bottom: var(--bs-card-group-margin); +} +@media (min-width: 576px) { + .card-group { + display: flex; + flex-flow: row wrap; + } + .card-group > .card { + flex: 1 0 0%; + margin-bottom: 0; + } + .card-group > .card + .card { + margin-left: 0; + border-left: 0; + } + .card-group > .card:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-top, + .card-group > .card:not(:last-child) .card-header { + border-top-right-radius: 0; + } + .card-group > .card:not(:last-child) .card-img-bottom, + .card-group > .card:not(:last-child) .card-footer { + border-bottom-right-radius: 0; + } + .card-group > .card:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-top, + .card-group > .card:not(:first-child) .card-header { + border-top-left-radius: 0; + } + .card-group > .card:not(:first-child) .card-img-bottom, + .card-group > .card:not(:first-child) .card-footer { + border-bottom-left-radius: 0; + } +} + +.accordion { + --bs-accordion-color: var(--bs-body-color); + --bs-accordion-bg: var(--bs-body-bg); + --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; + --bs-accordion-border-color: var(--bs-border-color); + --bs-accordion-border-width: var(--bs-border-width); + --bs-accordion-border-radius: var(--bs-border-radius); + --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-accordion-btn-padding-x: 1.25rem; + --bs-accordion-btn-padding-y: 1rem; + --bs-accordion-btn-color: var(--bs-body-color); + --bs-accordion-btn-bg: var(--bs-accordion-bg); + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-icon-width: 1.25rem; + --bs-accordion-btn-icon-transform: rotate(-180deg); + --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23052c65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-focus-border-color: #86b7fe; + --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-accordion-body-padding-x: 1.25rem; + --bs-accordion-body-padding-y: 1rem; + --bs-accordion-active-color: var(--bs-primary-text-emphasis); + --bs-accordion-active-bg: var(--bs-primary-bg-subtle); +} + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); + font-size: 1rem; + color: var(--bs-accordion-btn-color); + text-align: left; + background-color: var(--bs-accordion-btn-bg); + border: 0; + border-radius: 0; + overflow-anchor: none; + transition: var(--bs-accordion-transition); +} +@media (prefers-reduced-motion: reduce) { + .accordion-button { + transition: none; + } +} +.accordion-button:not(.collapsed) { + color: var(--bs-accordion-active-color); + background-color: var(--bs-accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); +} +.accordion-button:not(.collapsed)::after { + background-image: var(--bs-accordion-btn-active-icon); + transform: var(--bs-accordion-btn-icon-transform); +} +.accordion-button::after { + flex-shrink: 0; + width: var(--bs-accordion-btn-icon-width); + height: var(--bs-accordion-btn-icon-width); + margin-left: auto; + content: ""; + background-image: var(--bs-accordion-btn-icon); + background-repeat: no-repeat; + background-size: var(--bs-accordion-btn-icon-width); + transition: var(--bs-accordion-btn-icon-transition); +} +@media (prefers-reduced-motion: reduce) { + .accordion-button::after { + transition: none; + } +} +.accordion-button:hover { + z-index: 2; +} +.accordion-button:focus { + z-index: 3; + border-color: var(--bs-accordion-btn-focus-border-color); + outline: 0; + box-shadow: var(--bs-accordion-btn-focus-box-shadow); +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + color: var(--bs-accordion-color); + background-color: var(--bs-accordion-bg); + border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color); +} +.accordion-item:first-of-type { + border-top-left-radius: var(--bs-accordion-border-radius); + border-top-right-radius: var(--bs-accordion-border-radius); +} +.accordion-item:first-of-type .accordion-button { + border-top-left-radius: var(--bs-accordion-inner-border-radius); + border-top-right-radius: var(--bs-accordion-inner-border-radius); +} +.accordion-item:not(:first-of-type) { + border-top: 0; +} +.accordion-item:last-of-type { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} +.accordion-item:last-of-type .accordion-button.collapsed { + border-bottom-right-radius: var(--bs-accordion-inner-border-radius); + border-bottom-left-radius: var(--bs-accordion-inner-border-radius); +} +.accordion-item:last-of-type .accordion-collapse { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); +} + +.accordion-body { + padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); +} + +.accordion-flush .accordion-collapse { + border-width: 0; +} +.accordion-flush .accordion-item { + border-right: 0; + border-left: 0; + border-radius: 0; +} +.accordion-flush .accordion-item:first-child { + border-top: 0; +} +.accordion-flush .accordion-item:last-child { + border-bottom: 0; +} +.accordion-flush .accordion-item .accordion-button, .accordion-flush .accordion-item .accordion-button.collapsed { + border-radius: 0; +} + +[data-bs-theme=dark] .accordion-button::after { + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +.breadcrumb { + --bs-breadcrumb-padding-x: 0; + --bs-breadcrumb-padding-y: 0; + --bs-breadcrumb-margin-bottom: 1rem; + --bs-breadcrumb-bg: ; + --bs-breadcrumb-border-radius: ; + --bs-breadcrumb-divider-color: var(--bs-secondary-color); + --bs-breadcrumb-item-padding-x: 0.5rem; + --bs-breadcrumb-item-active-color: var(--bs-secondary-color); + display: flex; + flex-wrap: wrap; + padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x); + margin-bottom: var(--bs-breadcrumb-margin-bottom); + font-size: var(--bs-breadcrumb-font-size); + list-style: none; + background-color: var(--bs-breadcrumb-bg); + border-radius: var(--bs-breadcrumb-border-radius); +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: var(--bs-breadcrumb-item-padding-x); +} +.breadcrumb-item + .breadcrumb-item::before { + float: left; + padding-right: var(--bs-breadcrumb-item-padding-x); + color: var(--bs-breadcrumb-divider-color); + content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */; +} +.breadcrumb-item.active { + color: var(--bs-breadcrumb-item-active-color); +} + +.pagination { + --bs-pagination-padding-x: 0.75rem; + --bs-pagination-padding-y: 0.375rem; + --bs-pagination-font-size: 1rem; + --bs-pagination-color: var(--bs-link-color); + --bs-pagination-bg: var(--bs-body-bg); + --bs-pagination-border-width: var(--bs-border-width); + --bs-pagination-border-color: var(--bs-border-color); + --bs-pagination-border-radius: var(--bs-border-radius); + --bs-pagination-hover-color: var(--bs-link-hover-color); + --bs-pagination-hover-bg: var(--bs-tertiary-bg); + --bs-pagination-hover-border-color: var(--bs-border-color); + --bs-pagination-focus-color: var(--bs-link-hover-color); + --bs-pagination-focus-bg: var(--bs-secondary-bg); + --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-pagination-active-color: #fff; + --bs-pagination-active-bg: #0d6efd; + --bs-pagination-active-border-color: #0d6efd; + --bs-pagination-disabled-color: var(--bs-secondary-color); + --bs-pagination-disabled-bg: var(--bs-secondary-bg); + --bs-pagination-disabled-border-color: var(--bs-border-color); + display: flex; + padding-left: 0; + list-style: none; +} + +.page-link { + position: relative; + display: block; + padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x); + font-size: var(--bs-pagination-font-size); + color: var(--bs-pagination-color); + text-decoration: none; + background-color: var(--bs-pagination-bg); + border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color); + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .page-link { + transition: none; + } +} +.page-link:hover { + z-index: 2; + color: var(--bs-pagination-hover-color); + background-color: var(--bs-pagination-hover-bg); + border-color: var(--bs-pagination-hover-border-color); +} +.page-link:focus { + z-index: 3; + color: var(--bs-pagination-focus-color); + background-color: var(--bs-pagination-focus-bg); + outline: 0; + box-shadow: var(--bs-pagination-focus-box-shadow); +} +.page-link.active, .active > .page-link { + z-index: 3; + color: var(--bs-pagination-active-color); + background-color: var(--bs-pagination-active-bg); + border-color: var(--bs-pagination-active-border-color); +} +.page-link.disabled, .disabled > .page-link { + color: var(--bs-pagination-disabled-color); + pointer-events: none; + background-color: var(--bs-pagination-disabled-bg); + border-color: var(--bs-pagination-disabled-border-color); +} + +.page-item:not(:first-child) .page-link { + margin-left: calc(var(--bs-border-width) * -1); +} +.page-item:first-child .page-link { + border-top-left-radius: var(--bs-pagination-border-radius); + border-bottom-left-radius: var(--bs-pagination-border-radius); +} +.page-item:last-child .page-link { + border-top-right-radius: var(--bs-pagination-border-radius); + border-bottom-right-radius: var(--bs-pagination-border-radius); +} + +.pagination-lg { + --bs-pagination-padding-x: 1.5rem; + --bs-pagination-padding-y: 0.75rem; + --bs-pagination-font-size: 1.25rem; + --bs-pagination-border-radius: var(--bs-border-radius-lg); +} + +.pagination-sm { + --bs-pagination-padding-x: 0.5rem; + --bs-pagination-padding-y: 0.25rem; + --bs-pagination-font-size: 0.875rem; + --bs-pagination-border-radius: var(--bs-border-radius-sm); +} + +.badge { + --bs-badge-padding-x: 0.65em; + --bs-badge-padding-y: 0.35em; + --bs-badge-font-size: 0.75em; + --bs-badge-font-weight: 700; + --bs-badge-color: #fff; + --bs-badge-border-radius: var(--bs-border-radius); + display: inline-block; + padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x); + font-size: var(--bs-badge-font-size); + font-weight: var(--bs-badge-font-weight); + line-height: 1; + color: var(--bs-badge-color); + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: var(--bs-badge-border-radius); +} +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.alert { + --bs-alert-bg: transparent; + --bs-alert-padding-x: 1rem; + --bs-alert-padding-y: 1rem; + --bs-alert-margin-bottom: 1rem; + --bs-alert-color: inherit; + --bs-alert-border-color: transparent; + --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color); + --bs-alert-border-radius: var(--bs-border-radius); + --bs-alert-link-color: inherit; + position: relative; + padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x); + margin-bottom: var(--bs-alert-margin-bottom); + color: var(--bs-alert-color); + background-color: var(--bs-alert-bg); + border: var(--bs-alert-border); + border-radius: var(--bs-alert-border-radius); +} + +.alert-heading { + color: inherit; +} + +.alert-link { + font-weight: 700; + color: var(--bs-alert-link-color); +} + +.alert-dismissible { + padding-right: 3rem; +} +.alert-dismissible .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + padding: 1.25rem 1rem; +} + +.alert-primary { + --bs-alert-color: var(--bs-primary-text-emphasis); + --bs-alert-bg: var(--bs-primary-bg-subtle); + --bs-alert-border-color: var(--bs-primary-border-subtle); + --bs-alert-link-color: var(--bs-primary-text-emphasis); +} + +.alert-secondary { + --bs-alert-color: var(--bs-secondary-text-emphasis); + --bs-alert-bg: var(--bs-secondary-bg-subtle); + --bs-alert-border-color: var(--bs-secondary-border-subtle); + --bs-alert-link-color: var(--bs-secondary-text-emphasis); +} + +.alert-success { + --bs-alert-color: var(--bs-success-text-emphasis); + --bs-alert-bg: var(--bs-success-bg-subtle); + --bs-alert-border-color: var(--bs-success-border-subtle); + --bs-alert-link-color: var(--bs-success-text-emphasis); +} + +.alert-info { + --bs-alert-color: var(--bs-info-text-emphasis); + --bs-alert-bg: var(--bs-info-bg-subtle); + --bs-alert-border-color: var(--bs-info-border-subtle); + --bs-alert-link-color: var(--bs-info-text-emphasis); +} + +.alert-warning { + --bs-alert-color: var(--bs-warning-text-emphasis); + --bs-alert-bg: var(--bs-warning-bg-subtle); + --bs-alert-border-color: var(--bs-warning-border-subtle); + --bs-alert-link-color: var(--bs-warning-text-emphasis); +} + +.alert-danger { + --bs-alert-color: var(--bs-danger-text-emphasis); + --bs-alert-bg: var(--bs-danger-bg-subtle); + --bs-alert-border-color: var(--bs-danger-border-subtle); + --bs-alert-link-color: var(--bs-danger-text-emphasis); +} + +.alert-light { + --bs-alert-color: var(--bs-light-text-emphasis); + --bs-alert-bg: var(--bs-light-bg-subtle); + --bs-alert-border-color: var(--bs-light-border-subtle); + --bs-alert-link-color: var(--bs-light-text-emphasis); +} + +.alert-dark { + --bs-alert-color: var(--bs-dark-text-emphasis); + --bs-alert-bg: var(--bs-dark-bg-subtle); + --bs-alert-border-color: var(--bs-dark-border-subtle); + --bs-alert-link-color: var(--bs-dark-text-emphasis); +} + +@keyframes progress-bar-stripes { + 0% { + background-position-x: 1rem; + } +} +.progress, +.progress-stacked { + --bs-progress-height: 1rem; + --bs-progress-font-size: 0.75rem; + --bs-progress-bg: var(--bs-secondary-bg); + --bs-progress-border-radius: var(--bs-border-radius); + --bs-progress-box-shadow: var(--bs-box-shadow-inset); + --bs-progress-bar-color: #fff; + --bs-progress-bar-bg: #0d6efd; + --bs-progress-bar-transition: width 0.6s ease; + display: flex; + height: var(--bs-progress-height); + overflow: hidden; + font-size: var(--bs-progress-font-size); + background-color: var(--bs-progress-bg); + border-radius: var(--bs-progress-border-radius); +} + +.progress-bar { + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; + color: var(--bs-progress-bar-color); + text-align: center; + white-space: nowrap; + background-color: var(--bs-progress-bar-bg); + transition: var(--bs-progress-bar-transition); +} +@media (prefers-reduced-motion: reduce) { + .progress-bar { + transition: none; + } +} + +.progress-bar-striped { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: var(--bs-progress-height) var(--bs-progress-height); +} + +.progress-stacked > .progress { + overflow: visible; +} + +.progress-stacked > .progress > .progress-bar { + width: 100%; +} + +.progress-bar-animated { + animation: 1s linear infinite progress-bar-stripes; +} +@media (prefers-reduced-motion: reduce) { + .progress-bar-animated { + animation: none; + } +} + +.list-group { + --bs-list-group-color: var(--bs-body-color); + --bs-list-group-bg: var(--bs-body-bg); + --bs-list-group-border-color: var(--bs-border-color); + --bs-list-group-border-width: var(--bs-border-width); + --bs-list-group-border-radius: var(--bs-border-radius); + --bs-list-group-item-padding-x: 1rem; + --bs-list-group-item-padding-y: 0.5rem; + --bs-list-group-action-color: var(--bs-secondary-color); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-tertiary-bg); + --bs-list-group-action-active-color: var(--bs-body-color); + --bs-list-group-action-active-bg: var(--bs-secondary-bg); + --bs-list-group-disabled-color: var(--bs-secondary-color); + --bs-list-group-disabled-bg: var(--bs-body-bg); + --bs-list-group-active-color: #fff; + --bs-list-group-active-bg: #0d6efd; + --bs-list-group-active-border-color: #0d6efd; + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + border-radius: var(--bs-list-group-border-radius); +} + +.list-group-numbered { + list-style-type: none; + counter-reset: section; +} +.list-group-numbered > .list-group-item::before { + content: counters(section, ".") ". "; + counter-increment: section; +} + +.list-group-item-action { + width: 100%; + color: var(--bs-list-group-action-color); + text-align: inherit; +} +.list-group-item-action:hover, .list-group-item-action:focus { + z-index: 1; + color: var(--bs-list-group-action-hover-color); + text-decoration: none; + background-color: var(--bs-list-group-action-hover-bg); +} +.list-group-item-action:active { + color: var(--bs-list-group-action-active-color); + background-color: var(--bs-list-group-action-active-bg); +} + +.list-group-item { + position: relative; + display: block; + padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x); + color: var(--bs-list-group-color); + text-decoration: none; + background-color: var(--bs-list-group-bg); + border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); +} +.list-group-item:first-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} +.list-group-item:last-child { + border-bottom-right-radius: inherit; + border-bottom-left-radius: inherit; +} +.list-group-item.disabled, .list-group-item:disabled { + color: var(--bs-list-group-disabled-color); + pointer-events: none; + background-color: var(--bs-list-group-disabled-bg); +} +.list-group-item.active { + z-index: 2; + color: var(--bs-list-group-active-color); + background-color: var(--bs-list-group-active-bg); + border-color: var(--bs-list-group-active-border-color); +} +.list-group-item + .list-group-item { + border-top-width: 0; +} +.list-group-item + .list-group-item.active { + margin-top: calc(-1 * var(--bs-list-group-border-width)); + border-top-width: var(--bs-list-group-border-width); +} + +.list-group-horizontal { + flex-direction: row; +} +.list-group-horizontal > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; +} +.list-group-horizontal > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; +} +.list-group-horizontal > .list-group-item.active { + margin-top: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; +} +.list-group-horizontal > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); +} + +@media (min-width: 576px) { + .list-group-horizontal-sm { + flex-direction: row; + } + .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-sm > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-sm > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 768px) { + .list-group-horizontal-md { + flex-direction: row; + } + .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-md > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-md > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 992px) { + .list-group-horizontal-lg { + flex-direction: row; + } + .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-lg > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-lg > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 1200px) { + .list-group-horizontal-xl { + flex-direction: row; + } + .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-xl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-xl > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +@media (min-width: 1400px) { + .list-group-horizontal-xxl { + flex-direction: row; + } + .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); + border-top-right-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); + border-bottom-left-radius: 0; + } + .list-group-horizontal-xxl > .list-group-item.active { + margin-top: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item { + border-top-width: var(--bs-list-group-border-width); + border-left-width: 0; + } + .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); + } +} +.list-group-flush { + border-radius: 0; +} +.list-group-flush > .list-group-item { + border-width: 0 0 var(--bs-list-group-border-width); +} +.list-group-flush > .list-group-item:last-child { + border-bottom-width: 0; +} + +.list-group-item-primary { + --bs-list-group-color: var(--bs-primary-text-emphasis); + --bs-list-group-bg: var(--bs-primary-bg-subtle); + --bs-list-group-border-color: var(--bs-primary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-primary-border-subtle); + --bs-list-group-active-color: var(--bs-primary-bg-subtle); + --bs-list-group-active-bg: var(--bs-primary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-primary-text-emphasis); +} + +.list-group-item-secondary { + --bs-list-group-color: var(--bs-secondary-text-emphasis); + --bs-list-group-bg: var(--bs-secondary-bg-subtle); + --bs-list-group-border-color: var(--bs-secondary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle); + --bs-list-group-active-color: var(--bs-secondary-bg-subtle); + --bs-list-group-active-bg: var(--bs-secondary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis); +} + +.list-group-item-success { + --bs-list-group-color: var(--bs-success-text-emphasis); + --bs-list-group-bg: var(--bs-success-bg-subtle); + --bs-list-group-border-color: var(--bs-success-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-success-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-success-border-subtle); + --bs-list-group-active-color: var(--bs-success-bg-subtle); + --bs-list-group-active-bg: var(--bs-success-text-emphasis); + --bs-list-group-active-border-color: var(--bs-success-text-emphasis); +} + +.list-group-item-info { + --bs-list-group-color: var(--bs-info-text-emphasis); + --bs-list-group-bg: var(--bs-info-bg-subtle); + --bs-list-group-border-color: var(--bs-info-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-info-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-info-border-subtle); + --bs-list-group-active-color: var(--bs-info-bg-subtle); + --bs-list-group-active-bg: var(--bs-info-text-emphasis); + --bs-list-group-active-border-color: var(--bs-info-text-emphasis); +} + +.list-group-item-warning { + --bs-list-group-color: var(--bs-warning-text-emphasis); + --bs-list-group-bg: var(--bs-warning-bg-subtle); + --bs-list-group-border-color: var(--bs-warning-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-warning-border-subtle); + --bs-list-group-active-color: var(--bs-warning-bg-subtle); + --bs-list-group-active-bg: var(--bs-warning-text-emphasis); + --bs-list-group-active-border-color: var(--bs-warning-text-emphasis); +} + +.list-group-item-danger { + --bs-list-group-color: var(--bs-danger-text-emphasis); + --bs-list-group-bg: var(--bs-danger-bg-subtle); + --bs-list-group-border-color: var(--bs-danger-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-danger-border-subtle); + --bs-list-group-active-color: var(--bs-danger-bg-subtle); + --bs-list-group-active-bg: var(--bs-danger-text-emphasis); + --bs-list-group-active-border-color: var(--bs-danger-text-emphasis); +} + +.list-group-item-light { + --bs-list-group-color: var(--bs-light-text-emphasis); + --bs-list-group-bg: var(--bs-light-bg-subtle); + --bs-list-group-border-color: var(--bs-light-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-light-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-light-border-subtle); + --bs-list-group-active-color: var(--bs-light-bg-subtle); + --bs-list-group-active-bg: var(--bs-light-text-emphasis); + --bs-list-group-active-border-color: var(--bs-light-text-emphasis); +} + +.list-group-item-dark { + --bs-list-group-color: var(--bs-dark-text-emphasis); + --bs-list-group-bg: var(--bs-dark-bg-subtle); + --bs-list-group-border-color: var(--bs-dark-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-dark-border-subtle); + --bs-list-group-active-color: var(--bs-dark-bg-subtle); + --bs-list-group-active-bg: var(--bs-dark-text-emphasis); + --bs-list-group-active-border-color: var(--bs-dark-text-emphasis); +} + +.btn-close { + --bs-btn-close-color: #000; + --bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e"); + --bs-btn-close-opacity: 0.5; + --bs-btn-close-hover-opacity: 0.75; + --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); + --bs-btn-close-focus-opacity: 1; + --bs-btn-close-disabled-opacity: 0.25; + --bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%); + box-sizing: content-box; + width: 1em; + height: 1em; + padding: 0.25em 0.25em; + color: var(--bs-btn-close-color); + background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat; + border: 0; + border-radius: 0.375rem; + opacity: var(--bs-btn-close-opacity); +} +.btn-close:hover { + color: var(--bs-btn-close-color); + text-decoration: none; + opacity: var(--bs-btn-close-hover-opacity); +} +.btn-close:focus { + outline: 0; + box-shadow: var(--bs-btn-close-focus-shadow); + opacity: var(--bs-btn-close-focus-opacity); +} +.btn-close:disabled, .btn-close.disabled { + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + opacity: var(--bs-btn-close-disabled-opacity); +} + +.btn-close-white { + filter: var(--bs-btn-close-white-filter); +} + +[data-bs-theme=dark] .btn-close { + filter: var(--bs-btn-close-white-filter); +} + +.toast { + --bs-toast-zindex: 1090; + --bs-toast-padding-x: 0.75rem; + --bs-toast-padding-y: 0.5rem; + --bs-toast-spacing: 1.5rem; + --bs-toast-max-width: 350px; + --bs-toast-font-size: 0.875rem; + --bs-toast-color: ; + --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-border-width: var(--bs-border-width); + --bs-toast-border-color: var(--bs-border-color-translucent); + --bs-toast-border-radius: var(--bs-border-radius); + --bs-toast-box-shadow: var(--bs-box-shadow); + --bs-toast-header-color: var(--bs-secondary-color); + --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-header-border-color: var(--bs-border-color-translucent); + width: var(--bs-toast-max-width); + max-width: 100%; + font-size: var(--bs-toast-font-size); + color: var(--bs-toast-color); + pointer-events: auto; + background-color: var(--bs-toast-bg); + background-clip: padding-box; + border: var(--bs-toast-border-width) solid var(--bs-toast-border-color); + box-shadow: var(--bs-toast-box-shadow); + border-radius: var(--bs-toast-border-radius); +} +.toast.showing { + opacity: 0; +} +.toast:not(.show) { + display: none; +} + +.toast-container { + --bs-toast-zindex: 1090; + position: absolute; + z-index: var(--bs-toast-zindex); + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + max-width: 100%; + pointer-events: none; +} +.toast-container > :not(:last-child) { + margin-bottom: var(--bs-toast-spacing); +} + +.toast-header { + display: flex; + align-items: center; + padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x); + color: var(--bs-toast-header-color); + background-color: var(--bs-toast-header-bg); + background-clip: padding-box; + border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color); + border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); + border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); +} +.toast-header .btn-close { + margin-right: calc(-0.5 * var(--bs-toast-padding-x)); + margin-left: var(--bs-toast-padding-x); +} + +.toast-body { + padding: var(--bs-toast-padding-x); + word-wrap: break-word; +} + +.modal { + --bs-modal-zindex: 1055; + --bs-modal-width: 500px; + --bs-modal-padding: 1rem; + --bs-modal-margin: 0.5rem; + --bs-modal-color: ; + --bs-modal-bg: var(--bs-body-bg); + --bs-modal-border-color: var(--bs-border-color-translucent); + --bs-modal-border-width: var(--bs-border-width); + --bs-modal-border-radius: var(--bs-border-radius-lg); + --bs-modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width))); + --bs-modal-header-padding-x: 1rem; + --bs-modal-header-padding-y: 1rem; + --bs-modal-header-padding: 1rem 1rem; + --bs-modal-header-border-color: var(--bs-border-color); + --bs-modal-header-border-width: var(--bs-border-width); + --bs-modal-title-line-height: 1.5; + --bs-modal-footer-gap: 0.5rem; + --bs-modal-footer-bg: ; + --bs-modal-footer-border-color: var(--bs-border-color); + --bs-modal-footer-border-width: var(--bs-border-width); + position: fixed; + top: 0; + left: 0; + z-index: var(--bs-modal-zindex); + display: none; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; +} + +.modal-dialog { + position: relative; + width: auto; + margin: var(--bs-modal-margin); + pointer-events: none; +} +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: translate(0, -50px); +} +@media (prefers-reduced-motion: reduce) { + .modal.fade .modal-dialog { + transition: none; + } +} +.modal.show .modal-dialog { + transform: none; +} +.modal.modal-static .modal-dialog { + transform: scale(1.02); +} + +.modal-dialog-scrollable { + height: calc(100% - var(--bs-modal-margin) * 2); +} +.modal-dialog-scrollable .modal-content { + max-height: 100%; + overflow: hidden; +} +.modal-dialog-scrollable .modal-body { + overflow-y: auto; +} + +.modal-dialog-centered { + display: flex; + align-items: center; + min-height: calc(100% - var(--bs-modal-margin) * 2); +} + +.modal-content { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + color: var(--bs-modal-color); + pointer-events: auto; + background-color: var(--bs-modal-bg); + background-clip: padding-box; + border: var(--bs-modal-border-width) solid var(--bs-modal-border-color); + border-radius: var(--bs-modal-border-radius); + outline: 0; +} + +.modal-backdrop { + --bs-backdrop-zindex: 1050; + --bs-backdrop-bg: #000; + --bs-backdrop-opacity: 0.5; + position: fixed; + top: 0; + left: 0; + z-index: var(--bs-backdrop-zindex); + width: 100vw; + height: 100vh; + background-color: var(--bs-backdrop-bg); +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop.show { + opacity: var(--bs-backdrop-opacity); +} + +.modal-header { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + padding: var(--bs-modal-header-padding); + border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color); + border-top-left-radius: var(--bs-modal-inner-border-radius); + border-top-right-radius: var(--bs-modal-inner-border-radius); +} +.modal-header .btn-close { + padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5); + margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto; +} + +.modal-title { + margin-bottom: 0; + line-height: var(--bs-modal-title-line-height); +} + +.modal-body { + position: relative; + flex: 1 1 auto; + padding: var(--bs-modal-padding); +} + +.modal-footer { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5); + background-color: var(--bs-modal-footer-bg); + border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color); + border-bottom-right-radius: var(--bs-modal-inner-border-radius); + border-bottom-left-radius: var(--bs-modal-inner-border-radius); +} +.modal-footer > * { + margin: calc(var(--bs-modal-footer-gap) * 0.5); +} + +@media (min-width: 576px) { + .modal { + --bs-modal-margin: 1.75rem; + --bs-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + } + .modal-dialog { + max-width: var(--bs-modal-width); + margin-right: auto; + margin-left: auto; + } + .modal-sm { + --bs-modal-width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg, + .modal-xl { + --bs-modal-width: 800px; + } +} +@media (min-width: 1200px) { + .modal-xl { + --bs-modal-width: 1140px; + } +} +.modal-fullscreen { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; +} +.modal-fullscreen .modal-content { + height: 100%; + border: 0; + border-radius: 0; +} +.modal-fullscreen .modal-header, +.modal-fullscreen .modal-footer { + border-radius: 0; +} +.modal-fullscreen .modal-body { + overflow-y: auto; +} + +@media (max-width: 575.98px) { + .modal-fullscreen-sm-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-sm-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-header, + .modal-fullscreen-sm-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-sm-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 767.98px) { + .modal-fullscreen-md-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-md-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-md-down .modal-header, + .modal-fullscreen-md-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-md-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 991.98px) { + .modal-fullscreen-lg-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-lg-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-header, + .modal-fullscreen-lg-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-lg-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 1199.98px) { + .modal-fullscreen-xl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-header, + .modal-fullscreen-xl-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-xl-down .modal-body { + overflow-y: auto; + } +} +@media (max-width: 1399.98px) { + .modal-fullscreen-xxl-down { + width: 100vw; + max-width: none; + height: 100%; + margin: 0; + } + .modal-fullscreen-xxl-down .modal-content { + height: 100%; + border: 0; + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-header, + .modal-fullscreen-xxl-down .modal-footer { + border-radius: 0; + } + .modal-fullscreen-xxl-down .modal-body { + overflow-y: auto; + } +} +.tooltip { + --bs-tooltip-zindex: 1080; + --bs-tooltip-max-width: 200px; + --bs-tooltip-padding-x: 0.5rem; + --bs-tooltip-padding-y: 0.25rem; + --bs-tooltip-margin: ; + --bs-tooltip-font-size: 0.875rem; + --bs-tooltip-color: var(--bs-body-bg); + --bs-tooltip-bg: var(--bs-emphasis-color); + --bs-tooltip-border-radius: var(--bs-border-radius); + --bs-tooltip-opacity: 0.9; + --bs-tooltip-arrow-width: 0.8rem; + --bs-tooltip-arrow-height: 0.4rem; + z-index: var(--bs-tooltip-zindex); + display: block; + margin: var(--bs-tooltip-margin); + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + white-space: normal; + word-spacing: normal; + line-break: auto; + font-size: var(--bs-tooltip-font-size); + word-wrap: break-word; + opacity: 0; +} +.tooltip.show { + opacity: var(--bs-tooltip-opacity); +} +.tooltip .tooltip-arrow { + display: block; + width: var(--bs-tooltip-arrow-width); + height: var(--bs-tooltip-arrow-height); +} +.tooltip .tooltip-arrow::before { + position: absolute; + content: ""; + border-color: transparent; + border-style: solid; +} + +.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow { + bottom: calc(-1 * var(--bs-tooltip-arrow-height)); +} +.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before { + top: -1px; + border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-top-color: var(--bs-tooltip-bg); +} + +/* rtl:begin:ignore */ +.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow { + left: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); +} +.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before { + right: -1px; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-right-color: var(--bs-tooltip-bg); +} + +/* rtl:end:ignore */ +.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow { + top: calc(-1 * var(--bs-tooltip-arrow-height)); +} +.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before { + bottom: -1px; + border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-bottom-color: var(--bs-tooltip-bg); +} + +/* rtl:begin:ignore */ +.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow { + right: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); +} +.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before { + left: -1px; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-left-color: var(--bs-tooltip-bg); +} + +/* rtl:end:ignore */ +.tooltip-inner { + max-width: var(--bs-tooltip-max-width); + padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x); + color: var(--bs-tooltip-color); + text-align: center; + background-color: var(--bs-tooltip-bg); + border-radius: var(--bs-tooltip-border-radius); +} + +.popover { + --bs-popover-zindex: 1070; + --bs-popover-max-width: 276px; + --bs-popover-font-size: 0.875rem; + --bs-popover-bg: var(--bs-body-bg); + --bs-popover-border-width: var(--bs-border-width); + --bs-popover-border-color: var(--bs-border-color-translucent); + --bs-popover-border-radius: var(--bs-border-radius-lg); + --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width)); + --bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-popover-header-padding-x: 1rem; + --bs-popover-header-padding-y: 0.5rem; + --bs-popover-header-font-size: 1rem; + --bs-popover-header-color: inherit; + --bs-popover-header-bg: var(--bs-secondary-bg); + --bs-popover-body-padding-x: 1rem; + --bs-popover-body-padding-y: 1rem; + --bs-popover-body-color: var(--bs-body-color); + --bs-popover-arrow-width: 1rem; + --bs-popover-arrow-height: 0.5rem; + --bs-popover-arrow-border: var(--bs-popover-border-color); + z-index: var(--bs-popover-zindex); + display: block; + max-width: var(--bs-popover-max-width); + font-family: var(--bs-font-sans-serif); + font-style: normal; + font-weight: 400; + line-height: 1.5; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + white-space: normal; + word-spacing: normal; + line-break: auto; + font-size: var(--bs-popover-font-size); + word-wrap: break-word; + background-color: var(--bs-popover-bg); + background-clip: padding-box; + border: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-radius: var(--bs-popover-border-radius); +} +.popover .popover-arrow { + display: block; + width: var(--bs-popover-arrow-width); + height: var(--bs-popover-arrow-height); +} +.popover .popover-arrow::before, .popover .popover-arrow::after { + position: absolute; + display: block; + content: ""; + border-color: transparent; + border-style: solid; + border-width: 0; +} + +.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { + bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { + bottom: 0; + border-top-color: var(--bs-popover-arrow-border); +} +.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + bottom: var(--bs-popover-border-width); + border-top-color: var(--bs-popover-bg); +} + +/* rtl:begin:ignore */ +.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { + left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { + left: 0; + border-right-color: var(--bs-popover-arrow-border); +} +.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + left: var(--bs-popover-border-width); + border-right-color: var(--bs-popover-bg); +} + +/* rtl:end:ignore */ +.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { + top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before { + top: 0; + border-bottom-color: var(--bs-popover-arrow-border); +} +.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + top: var(--bs-popover-border-width); + border-bottom-color: var(--bs-popover-bg); +} +.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: var(--bs-popover-arrow-width); + margin-left: calc(-0.5 * var(--bs-popover-arrow-width)); + content: ""; + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg); +} + +/* rtl:begin:ignore */ +.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { + right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { + right: 0; + border-left-color: var(--bs-popover-arrow-border); +} +.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + right: var(--bs-popover-border-width); + border-left-color: var(--bs-popover-bg); +} + +/* rtl:end:ignore */ +.popover-header { + padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x); + margin-bottom: 0; + font-size: var(--bs-popover-header-font-size); + color: var(--bs-popover-header-color); + background-color: var(--bs-popover-header-bg); + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-top-left-radius: var(--bs-popover-inner-border-radius); + border-top-right-radius: var(--bs-popover-inner-border-radius); +} +.popover-header:empty { + display: none; +} + +.popover-body { + padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x); + color: var(--bs-popover-body-color); +} + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner::after { + display: block; + clear: both; + content: ""; +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + transition: transform 0.6s ease-in-out; +} +@media (prefers-reduced-motion: reduce) { + .carousel-item { + transition: none; + } +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + +.carousel-fade .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; +} +.carousel-fade .carousel-item.active, +.carousel-fade .carousel-item-next.carousel-item-start, +.carousel-fade .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; +} +.carousel-fade .active.carousel-item-start, +.carousel-fade .active.carousel-item-end { + z-index: 0; + opacity: 0; + transition: opacity 0s 0.6s; +} +@media (prefers-reduced-motion: reduce) { + .carousel-fade .active.carousel-item-start, + .carousel-fade .active.carousel-item-end { + transition: none; + } +} + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 15%; + padding: 0; + color: #fff; + text-align: center; + background: none; + border: 0; + opacity: 0.5; + transition: opacity 0.15s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-control-prev, + .carousel-control-next { + transition: none; + } +} +.carousel-control-prev:hover, .carousel-control-prev:focus, +.carousel-control-next:hover, +.carousel-control-next:focus { + color: #fff; + text-decoration: none; + outline: 0; + opacity: 0.9; +} + +.carousel-control-prev { + left: 0; +} + +.carousel-control-next { + right: 0; +} + +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: 2rem; + height: 2rem; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +/* rtl:options: { + "autoRename": true, + "stringMap":[ { + "name" : "prev-next", + "search" : "prev", + "replace" : "next" + } ] +} */ +.carousel-control-prev-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e"); +} + +.carousel-control-next-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + margin-right: 15%; + margin-bottom: 1rem; + margin-left: 15%; +} +.carousel-indicators [data-bs-target] { + box-sizing: content-box; + flex: 0 1 auto; + width: 30px; + height: 3px; + padding: 0; + margin-right: 3px; + margin-left: 3px; + text-indent: -999px; + cursor: pointer; + background-color: #fff; + background-clip: padding-box; + border: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + opacity: 0.5; + transition: opacity 0.6s ease; +} +@media (prefers-reduced-motion: reduce) { + .carousel-indicators [data-bs-target] { + transition: none; + } +} +.carousel-indicators .active { + opacity: 1; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 1.25rem; + left: 15%; + padding-top: 1.25rem; + padding-bottom: 1.25rem; + color: #fff; + text-align: center; +} + +.carousel-dark .carousel-control-prev-icon, +.carousel-dark .carousel-control-next-icon { + filter: invert(1) grayscale(100); +} +.carousel-dark .carousel-indicators [data-bs-target] { + background-color: #000; +} +.carousel-dark .carousel-caption { + color: #000; +} + +[data-bs-theme=dark] .carousel .carousel-control-prev-icon, +[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon, +[data-bs-theme=dark].carousel .carousel-control-next-icon { + filter: invert(1) grayscale(100); +} +[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] { + background-color: #000; +} +[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption { + color: #000; +} + +.spinner-grow, +.spinner-border { + display: inline-block; + width: var(--bs-spinner-width); + height: var(--bs-spinner-height); + vertical-align: var(--bs-spinner-vertical-align); + border-radius: 50%; + animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); +} + +@keyframes spinner-border { + to { + transform: rotate(360deg) /* rtl:ignore */; + } +} +.spinner-border { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-border-width: 0.25em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-border; + border: var(--bs-spinner-border-width) solid currentcolor; + border-right-color: transparent; +} + +.spinner-border-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; + --bs-spinner-border-width: 0.2em; +} + +@keyframes spinner-grow { + 0% { + transform: scale(0); + } + 50% { + opacity: 1; + transform: none; + } +} +.spinner-grow { + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-grow; + background-color: currentcolor; + opacity: 0; +} + +.spinner-grow-sm { + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; +} + +@media (prefers-reduced-motion: reduce) { + .spinner-border, + .spinner-grow { + --bs-spinner-animation-speed: 1.5s; + } +} +.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm { + --bs-offcanvas-zindex: 1045; + --bs-offcanvas-width: 400px; + --bs-offcanvas-height: 30vh; + --bs-offcanvas-padding-x: 1rem; + --bs-offcanvas-padding-y: 1rem; + --bs-offcanvas-color: var(--bs-body-color); + --bs-offcanvas-bg: var(--bs-body-bg); + --bs-offcanvas-border-width: var(--bs-border-width); + --bs-offcanvas-border-color: var(--bs-border-color-translucent); + --bs-offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-offcanvas-transition: transform 0.3s ease-in-out; + --bs-offcanvas-title-line-height: 1.5; +} + +@media (max-width: 575.98px) { + .offcanvas-sm { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-sm { + transition: none; + } +} +@media (max-width: 575.98px) { + .offcanvas-sm.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-sm.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-sm.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-sm.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) { + transform: none; + } + .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show { + visibility: visible; + } +} +@media (min-width: 576px) { + .offcanvas-sm { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-sm .offcanvas-header { + display: none; + } + .offcanvas-sm .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 767.98px) { + .offcanvas-md { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-md { + transition: none; + } +} +@media (max-width: 767.98px) { + .offcanvas-md.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-md.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-md.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-md.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) { + transform: none; + } + .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show { + visibility: visible; + } +} +@media (min-width: 768px) { + .offcanvas-md { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-md .offcanvas-header { + display: none; + } + .offcanvas-md .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 991.98px) { + .offcanvas-lg { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-lg { + transition: none; + } +} +@media (max-width: 991.98px) { + .offcanvas-lg.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-lg.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-lg.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-lg.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) { + transform: none; + } + .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show { + visibility: visible; + } +} +@media (min-width: 992px) { + .offcanvas-lg { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-lg .offcanvas-header { + display: none; + } + .offcanvas-lg .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1199.98px) { + .offcanvas-xl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xl { + transition: none; + } +} +@media (max-width: 1199.98px) { + .offcanvas-xl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) { + transform: none; + } + .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show { + visibility: visible; + } +} +@media (min-width: 1200px) { + .offcanvas-xl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xl .offcanvas-header { + display: none; + } + .offcanvas-xl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1399.98px) { + .offcanvas-xxl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xxl { + transition: none; + } +} +@media (max-width: 1399.98px) { + .offcanvas-xxl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xxl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xxl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xxl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) { + transform: none; + } + .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show { + visibility: visible; + } +} +@media (min-width: 1400px) { + .offcanvas-xxl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xxl .offcanvas-header { + display: none; + } + .offcanvas-xxl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +.offcanvas { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); +} +@media (prefers-reduced-motion: reduce) { + .offcanvas { + transition: none; + } +} +.offcanvas.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); +} +.offcanvas.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); +} +.offcanvas.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); +} +.offcanvas.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); +} +.offcanvas.showing, .offcanvas.show:not(.hiding) { + transform: none; +} +.offcanvas.showing, .offcanvas.hiding, .offcanvas.show { + visibility: visible; +} + +.offcanvas-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 1040; + width: 100vw; + height: 100vh; + background-color: #000; +} +.offcanvas-backdrop.fade { + opacity: 0; +} +.offcanvas-backdrop.show { + opacity: 0.5; +} + +.offcanvas-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); +} +.offcanvas-header .btn-close { + padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5); + margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x)); + margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y)); +} + +.offcanvas-title { + margin-bottom: 0; + line-height: var(--bs-offcanvas-title-line-height); +} + +.offcanvas-body { + flex-grow: 1; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); + overflow-y: auto; +} + +.placeholder { + display: inline-block; + min-height: 1em; + vertical-align: middle; + cursor: wait; + background-color: currentcolor; + opacity: 0.5; +} +.placeholder.btn::before { + display: inline-block; + content: ""; +} + +.placeholder-xs { + min-height: 0.6em; +} + +.placeholder-sm { + min-height: 0.8em; +} + +.placeholder-lg { + min-height: 1.2em; +} + +.placeholder-glow .placeholder { + animation: placeholder-glow 2s ease-in-out infinite; +} + +@keyframes placeholder-glow { + 50% { + opacity: 0.2; + } +} +.placeholder-wave { + -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); + mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); + -webkit-mask-size: 200% 100%; + mask-size: 200% 100%; + animation: placeholder-wave 2s linear infinite; +} + +@keyframes placeholder-wave { + 100% { + -webkit-mask-position: -200% 0%; + mask-position: -200% 0%; + } +} +.clearfix::after { + display: block; + clear: both; + content: ""; +} + +.text-bg-primary { + color: #fff !important; + background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-secondary { + color: #fff !important; + background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-success { + color: #fff !important; + background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-info { + color: #000 !important; + background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-warning { + color: #000 !important; + background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-danger { + color: #fff !important; + background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-light { + color: #000 !important; + background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-dark { + color: #fff !important; + background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.link-primary { + color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-primary:hover, .link-primary:focus { + color: RGBA(10, 88, 202, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(10, 88, 202, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-secondary { + color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-secondary:hover, .link-secondary:focus { + color: RGBA(86, 94, 100, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(86, 94, 100, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-success { + color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-success:hover, .link-success:focus { + color: RGBA(20, 108, 67, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(20, 108, 67, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-info { + color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-info:hover, .link-info:focus { + color: RGBA(61, 213, 243, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(61, 213, 243, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-warning { + color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-warning:hover, .link-warning:focus { + color: RGBA(255, 205, 57, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(255, 205, 57, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-danger { + color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-danger:hover, .link-danger:focus { + color: RGBA(176, 42, 55, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(176, 42, 55, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-light { + color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-light:hover, .link-light:focus { + color: RGBA(249, 250, 251, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(249, 250, 251, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-dark { + color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-dark:hover, .link-dark:focus { + color: RGBA(26, 30, 33, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(26, 30, 33, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-body-emphasis { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-body-emphasis:hover, .link-body-emphasis:focus { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; +} + +.focus-ring:focus { + outline: 0; + box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color); +} + +.icon-link { + display: inline-flex; + gap: 0.375rem; + align-items: center; + -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); + text-underline-offset: 0.25em; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} +.icon-link > .bi { + flex-shrink: 0; + width: 1em; + height: 1em; + fill: currentcolor; + transition: 0.2s ease-in-out transform; +} +@media (prefers-reduced-motion: reduce) { + .icon-link > .bi { + transition: none; + } +} + +.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi { + transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0)); +} + +.ratio { + position: relative; + width: 100%; +} +.ratio::before { + display: block; + padding-top: var(--bs-aspect-ratio); + content: ""; +} +.ratio > * { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.ratio-1x1 { + --bs-aspect-ratio: 100%; +} + +.ratio-4x3 { + --bs-aspect-ratio: 75%; +} + +.ratio-16x9 { + --bs-aspect-ratio: 56.25%; +} + +.ratio-21x9 { + --bs-aspect-ratio: 42.8571428571%; +} + +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} + +.fixed-bottom { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; +} + +.sticky-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; +} + +.sticky-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; +} + +@media (min-width: 576px) { + .sticky-sm-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-sm-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 768px) { + .sticky-md-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-md-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 992px) { + .sticky-lg-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-lg-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 1200px) { + .sticky-xl-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-xl-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +@media (min-width: 1400px) { + .sticky-xxl-top { + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 1020; + } + .sticky-xxl-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } +} +.hstack { + display: flex; + flex-direction: row; + align-items: center; + align-self: stretch; +} + +.vstack { + display: flex; + flex: 1 1 auto; + flex-direction: column; + align-self: stretch; +} + +.visually-hidden, +.visually-hidden-focusable:not(:focus):not(:focus-within) { + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} +.visually-hidden:not(caption), +.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) { + position: absolute !important; +} + +.stretched-link::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: ""; +} + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.vr { + display: inline-block; + align-self: stretch; + width: var(--bs-border-width); + min-height: 1em; + background-color: currentcolor; + opacity: 0.25; +} + +.align-baseline { + vertical-align: baseline !important; +} + +.align-top { + vertical-align: top !important; +} + +.align-middle { + vertical-align: middle !important; +} + +.align-bottom { + vertical-align: bottom !important; +} + +.align-text-bottom { + vertical-align: text-bottom !important; +} + +.align-text-top { + vertical-align: text-top !important; +} + +.float-start { + float: left !important; +} + +.float-end { + float: right !important; +} + +.float-none { + float: none !important; +} + +.object-fit-contain { + -o-object-fit: contain !important; + object-fit: contain !important; +} + +.object-fit-cover { + -o-object-fit: cover !important; + object-fit: cover !important; +} + +.object-fit-fill { + -o-object-fit: fill !important; + object-fit: fill !important; +} + +.object-fit-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; +} + +.object-fit-none { + -o-object-fit: none !important; + object-fit: none !important; +} + +.opacity-0 { + opacity: 0 !important; +} + +.opacity-25 { + opacity: 0.25 !important; +} + +.opacity-50 { + opacity: 0.5 !important; +} + +.opacity-75 { + opacity: 0.75 !important; +} + +.opacity-100 { + opacity: 1 !important; +} + +.overflow-auto { + overflow: auto !important; +} + +.overflow-hidden { + overflow: hidden !important; +} + +.overflow-visible { + overflow: visible !important; +} + +.overflow-scroll { + overflow: scroll !important; +} + +.overflow-x-auto { + overflow-x: auto !important; +} + +.overflow-x-hidden { + overflow-x: hidden !important; +} + +.overflow-x-visible { + overflow-x: visible !important; +} + +.overflow-x-scroll { + overflow-x: scroll !important; +} + +.overflow-y-auto { + overflow-y: auto !important; +} + +.overflow-y-hidden { + overflow-y: hidden !important; +} + +.overflow-y-visible { + overflow-y: visible !important; +} + +.overflow-y-scroll { + overflow-y: scroll !important; +} + +.d-inline { + display: inline !important; +} + +.d-inline-block { + display: inline-block !important; +} + +.d-block { + display: block !important; +} + +.d-grid { + display: grid !important; +} + +.d-inline-grid { + display: inline-grid !important; +} + +.d-table { + display: table !important; +} + +.d-table-row { + display: table-row !important; +} + +.d-table-cell { + display: table-cell !important; +} + +.d-flex { + display: flex !important; +} + +.d-inline-flex { + display: inline-flex !important; +} + +.d-none { + display: none !important; +} + +.shadow { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.shadow-sm { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; +} + +.shadow-lg { + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; +} + +.shadow-none { + box-shadow: none !important; +} + +.focus-ring-primary { + --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-secondary { + --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-success { + --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-info { + --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-warning { + --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-danger { + --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-light { + --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-dark { + --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity)); +} + +.position-static { + position: static !important; +} + +.position-relative { + position: relative !important; +} + +.position-absolute { + position: absolute !important; +} + +.position-fixed { + position: fixed !important; +} + +.position-sticky { + position: -webkit-sticky !important; + position: sticky !important; +} + +.top-0 { + top: 0 !important; +} + +.top-50 { + top: 50% !important; +} + +.top-100 { + top: 100% !important; +} + +.bottom-0 { + bottom: 0 !important; +} + +.bottom-50 { + bottom: 50% !important; +} + +.bottom-100 { + bottom: 100% !important; +} + +.start-0 { + left: 0 !important; +} + +.start-50 { + left: 50% !important; +} + +.start-100 { + left: 100% !important; +} + +.end-0 { + right: 0 !important; +} + +.end-50 { + right: 50% !important; +} + +.end-100 { + right: 100% !important; +} + +.translate-middle { + transform: translate(-50%, -50%) !important; +} + +.translate-middle-x { + transform: translateX(-50%) !important; +} + +.translate-middle-y { + transform: translateY(-50%) !important; +} + +.border { + border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-0 { + border: 0 !important; +} + +.border-top { + border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-top-0 { + border-top: 0 !important; +} + +.border-end { + border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-end-0 { + border-right: 0 !important; +} + +.border-bottom { + border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-bottom-0 { + border-bottom: 0 !important; +} + +.border-start { + border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; +} + +.border-start-0 { + border-left: 0 !important; +} + +.border-primary { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important; +} + +.border-secondary { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important; +} + +.border-success { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important; +} + +.border-info { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important; +} + +.border-warning { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important; +} + +.border-danger { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important; +} + +.border-light { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important; +} + +.border-dark { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important; +} + +.border-black { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important; +} + +.border-white { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important; +} + +.border-primary-subtle { + border-color: var(--bs-primary-border-subtle) !important; +} + +.border-secondary-subtle { + border-color: var(--bs-secondary-border-subtle) !important; +} + +.border-success-subtle { + border-color: var(--bs-success-border-subtle) !important; +} + +.border-info-subtle { + border-color: var(--bs-info-border-subtle) !important; +} + +.border-warning-subtle { + border-color: var(--bs-warning-border-subtle) !important; +} + +.border-danger-subtle { + border-color: var(--bs-danger-border-subtle) !important; +} + +.border-light-subtle { + border-color: var(--bs-light-border-subtle) !important; +} + +.border-dark-subtle { + border-color: var(--bs-dark-border-subtle) !important; +} + +.border-1 { + border-width: 1px !important; +} + +.border-2 { + border-width: 2px !important; +} + +.border-3 { + border-width: 3px !important; +} + +.border-4 { + border-width: 4px !important; +} + +.border-5 { + border-width: 5px !important; +} + +.border-opacity-10 { + --bs-border-opacity: 0.1; +} + +.border-opacity-25 { + --bs-border-opacity: 0.25; +} + +.border-opacity-50 { + --bs-border-opacity: 0.5; +} + +.border-opacity-75 { + --bs-border-opacity: 0.75; +} + +.border-opacity-100 { + --bs-border-opacity: 1; +} + +.w-25 { + width: 25% !important; +} + +.w-50 { + width: 50% !important; +} + +.w-75 { + width: 75% !important; +} + +.w-100 { + width: 100% !important; +} + +.w-auto { + width: auto !important; +} + +.mw-100 { + max-width: 100% !important; +} + +.vw-100 { + width: 100vw !important; +} + +.min-vw-100 { + min-width: 100vw !important; +} + +.h-25 { + height: 25% !important; +} + +.h-50 { + height: 50% !important; +} + +.h-75 { + height: 75% !important; +} + +.h-100 { + height: 100% !important; +} + +.h-auto { + height: auto !important; +} + +.mh-100 { + max-height: 100% !important; +} + +.vh-100 { + height: 100vh !important; +} + +.min-vh-100 { + min-height: 100vh !important; +} + +.flex-fill { + flex: 1 1 auto !important; +} + +.flex-row { + flex-direction: row !important; +} + +.flex-column { + flex-direction: column !important; +} + +.flex-row-reverse { + flex-direction: row-reverse !important; +} + +.flex-column-reverse { + flex-direction: column-reverse !important; +} + +.flex-grow-0 { + flex-grow: 0 !important; +} + +.flex-grow-1 { + flex-grow: 1 !important; +} + +.flex-shrink-0 { + flex-shrink: 0 !important; +} + +.flex-shrink-1 { + flex-shrink: 1 !important; +} + +.flex-wrap { + flex-wrap: wrap !important; +} + +.flex-nowrap { + flex-wrap: nowrap !important; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse !important; +} + +.justify-content-start { + justify-content: flex-start !important; +} + +.justify-content-end { + justify-content: flex-end !important; +} + +.justify-content-center { + justify-content: center !important; +} + +.justify-content-between { + justify-content: space-between !important; +} + +.justify-content-around { + justify-content: space-around !important; +} + +.justify-content-evenly { + justify-content: space-evenly !important; +} + +.align-items-start { + align-items: flex-start !important; +} + +.align-items-end { + align-items: flex-end !important; +} + +.align-items-center { + align-items: center !important; +} + +.align-items-baseline { + align-items: baseline !important; +} + +.align-items-stretch { + align-items: stretch !important; +} + +.align-content-start { + align-content: flex-start !important; +} + +.align-content-end { + align-content: flex-end !important; +} + +.align-content-center { + align-content: center !important; +} + +.align-content-between { + align-content: space-between !important; +} + +.align-content-around { + align-content: space-around !important; +} + +.align-content-stretch { + align-content: stretch !important; +} + +.align-self-auto { + align-self: auto !important; +} + +.align-self-start { + align-self: flex-start !important; +} + +.align-self-end { + align-self: flex-end !important; +} + +.align-self-center { + align-self: center !important; +} + +.align-self-baseline { + align-self: baseline !important; +} + +.align-self-stretch { + align-self: stretch !important; +} + +.order-first { + order: -1 !important; +} + +.order-0 { + order: 0 !important; +} + +.order-1 { + order: 1 !important; +} + +.order-2 { + order: 2 !important; +} + +.order-3 { + order: 3 !important; +} + +.order-4 { + order: 4 !important; +} + +.order-5 { + order: 5 !important; +} + +.order-last { + order: 6 !important; +} + +.m-0 { + margin: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 1rem !important; +} + +.m-4 { + margin: 1.5rem !important; +} + +.m-5 { + margin: 3rem !important; +} + +.m-auto { + margin: auto !important; +} + +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} + +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} + +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} + +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} + +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} + +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} + +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mt-3 { + margin-top: 1rem !important; +} + +.mt-4 { + margin-top: 1.5rem !important; +} + +.mt-5 { + margin-top: 3rem !important; +} + +.mt-auto { + margin-top: auto !important; +} + +.me-0 { + margin-right: 0 !important; +} + +.me-1 { + margin-right: 0.25rem !important; +} + +.me-2 { + margin-right: 0.5rem !important; +} + +.me-3 { + margin-right: 1rem !important; +} + +.me-4 { + margin-right: 1.5rem !important; +} + +.me-5 { + margin-right: 3rem !important; +} + +.me-auto { + margin-right: auto !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 3rem !important; +} + +.mb-auto { + margin-bottom: auto !important; +} + +.ms-0 { + margin-left: 0 !important; +} + +.ms-1 { + margin-left: 0.25rem !important; +} + +.ms-2 { + margin-left: 0.5rem !important; +} + +.ms-3 { + margin-left: 1rem !important; +} + +.ms-4 { + margin-left: 1.5rem !important; +} + +.ms-5 { + margin-left: 3rem !important; +} + +.ms-auto { + margin-left: auto !important; +} + +.p-0 { + padding: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.p-3 { + padding: 1rem !important; +} + +.p-4 { + padding: 1.5rem !important; +} + +.p-5 { + padding: 3rem !important; +} + +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} + +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} + +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; +} + +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} + +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} + +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pt-3 { + padding-top: 1rem !important; +} + +.pt-4 { + padding-top: 1.5rem !important; +} + +.pt-5 { + padding-top: 3rem !important; +} + +.pe-0 { + padding-right: 0 !important; +} + +.pe-1 { + padding-right: 0.25rem !important; +} + +.pe-2 { + padding-right: 0.5rem !important; +} + +.pe-3 { + padding-right: 1rem !important; +} + +.pe-4 { + padding-right: 1.5rem !important; +} + +.pe-5 { + padding-right: 3rem !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pb-3 { + padding-bottom: 1rem !important; +} + +.pb-4 { + padding-bottom: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 3rem !important; +} + +.ps-0 { + padding-left: 0 !important; +} + +.ps-1 { + padding-left: 0.25rem !important; +} + +.ps-2 { + padding-left: 0.5rem !important; +} + +.ps-3 { + padding-left: 1rem !important; +} + +.ps-4 { + padding-left: 1.5rem !important; +} + +.ps-5 { + padding-left: 3rem !important; +} + +.gap-0 { + gap: 0 !important; +} + +.gap-1 { + gap: 0.25rem !important; +} + +.gap-2 { + gap: 0.5rem !important; +} + +.gap-3 { + gap: 1rem !important; +} + +.gap-4 { + gap: 1.5rem !important; +} + +.gap-5 { + gap: 3rem !important; +} + +.row-gap-0 { + row-gap: 0 !important; +} + +.row-gap-1 { + row-gap: 0.25rem !important; +} + +.row-gap-2 { + row-gap: 0.5rem !important; +} + +.row-gap-3 { + row-gap: 1rem !important; +} + +.row-gap-4 { + row-gap: 1.5rem !important; +} + +.row-gap-5 { + row-gap: 3rem !important; +} + +.column-gap-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; +} + +.column-gap-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; +} + +.column-gap-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; +} + +.column-gap-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; +} + +.column-gap-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; +} + +.column-gap-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; +} + +.font-monospace { + font-family: var(--bs-font-monospace) !important; +} + +.fs-1 { + font-size: calc(1.375rem + 1.5vw) !important; +} + +.fs-2 { + font-size: calc(1.325rem + 0.9vw) !important; +} + +.fs-3 { + font-size: calc(1.3rem + 0.6vw) !important; +} + +.fs-4 { + font-size: calc(1.275rem + 0.3vw) !important; +} + +.fs-5 { + font-size: 1.25rem !important; +} + +.fs-6 { + font-size: 1rem !important; +} + +.fst-italic { + font-style: italic !important; +} + +.fst-normal { + font-style: normal !important; +} + +.fw-lighter { + font-weight: lighter !important; +} + +.fw-light { + font-weight: 300 !important; +} + +.fw-normal { + font-weight: 400 !important; +} + +.fw-medium { + font-weight: 500 !important; +} + +.fw-semibold { + font-weight: 600 !important; +} + +.fw-bold { + font-weight: 700 !important; +} + +.fw-bolder { + font-weight: bolder !important; +} + +.lh-1 { + line-height: 1 !important; +} + +.lh-sm { + line-height: 1.25 !important; +} + +.lh-base { + line-height: 1.5 !important; +} + +.lh-lg { + line-height: 2 !important; +} + +.text-start { + text-align: left !important; +} + +.text-end { + text-align: right !important; +} + +.text-center { + text-align: center !important; +} + +.text-decoration-none { + text-decoration: none !important; +} + +.text-decoration-underline { + text-decoration: underline !important; +} + +.text-decoration-line-through { + text-decoration: line-through !important; +} + +.text-lowercase { + text-transform: lowercase !important; +} + +.text-uppercase { + text-transform: uppercase !important; +} + +.text-capitalize { + text-transform: capitalize !important; +} + +.text-wrap { + white-space: normal !important; +} + +.text-nowrap { + white-space: nowrap !important; +} + +/* rtl:begin:remove */ +.text-break { + word-wrap: break-word !important; + word-break: break-word !important; +} + +/* rtl:end:remove */ +.text-primary { + --bs-text-opacity: 1; + color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; +} + +.text-secondary { + --bs-text-opacity: 1; + color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; +} + +.text-success { + --bs-text-opacity: 1; + color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; +} + +.text-info { + --bs-text-opacity: 1; + color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; +} + +.text-warning { + --bs-text-opacity: 1; + color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; +} + +.text-danger { + --bs-text-opacity: 1; + color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; +} + +.text-light { + --bs-text-opacity: 1; + color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; +} + +.text-dark { + --bs-text-opacity: 1; + color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; +} + +.text-black { + --bs-text-opacity: 1; + color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; +} + +.text-white { + --bs-text-opacity: 1; + color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; +} + +.text-body { + --bs-text-opacity: 1; + color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important; +} + +.text-muted { + --bs-text-opacity: 1; + color: var(--bs-secondary-color) !important; +} + +.text-black-50 { + --bs-text-opacity: 1; + color: rgba(0, 0, 0, 0.5) !important; +} + +.text-white-50 { + --bs-text-opacity: 1; + color: rgba(255, 255, 255, 0.5) !important; +} + +.text-body-secondary { + --bs-text-opacity: 1; + color: var(--bs-secondary-color) !important; +} + +.text-body-tertiary { + --bs-text-opacity: 1; + color: var(--bs-tertiary-color) !important; +} + +.text-body-emphasis { + --bs-text-opacity: 1; + color: var(--bs-emphasis-color) !important; +} + +.text-reset { + --bs-text-opacity: 1; + color: inherit !important; +} + +.text-opacity-25 { + --bs-text-opacity: 0.25; +} + +.text-opacity-50 { + --bs-text-opacity: 0.5; +} + +.text-opacity-75 { + --bs-text-opacity: 0.75; +} + +.text-opacity-100 { + --bs-text-opacity: 1; +} + +.text-primary-emphasis { + color: var(--bs-primary-text-emphasis) !important; +} + +.text-secondary-emphasis { + color: var(--bs-secondary-text-emphasis) !important; +} + +.text-success-emphasis { + color: var(--bs-success-text-emphasis) !important; +} + +.text-info-emphasis { + color: var(--bs-info-text-emphasis) !important; +} + +.text-warning-emphasis { + color: var(--bs-warning-text-emphasis) !important; +} + +.text-danger-emphasis { + color: var(--bs-danger-text-emphasis) !important; +} + +.text-light-emphasis { + color: var(--bs-light-text-emphasis) !important; +} + +.text-dark-emphasis { + color: var(--bs-dark-text-emphasis) !important; +} + +.link-opacity-10 { + --bs-link-opacity: 0.1; +} + +.link-opacity-10-hover:hover { + --bs-link-opacity: 0.1; +} + +.link-opacity-25 { + --bs-link-opacity: 0.25; +} + +.link-opacity-25-hover:hover { + --bs-link-opacity: 0.25; +} + +.link-opacity-50 { + --bs-link-opacity: 0.5; +} + +.link-opacity-50-hover:hover { + --bs-link-opacity: 0.5; +} + +.link-opacity-75 { + --bs-link-opacity: 0.75; +} + +.link-opacity-75-hover:hover { + --bs-link-opacity: 0.75; +} + +.link-opacity-100 { + --bs-link-opacity: 1; +} + +.link-opacity-100-hover:hover { + --bs-link-opacity: 1; +} + +.link-offset-1 { + text-underline-offset: 0.125em !important; +} + +.link-offset-1-hover:hover { + text-underline-offset: 0.125em !important; +} + +.link-offset-2 { + text-underline-offset: 0.25em !important; +} + +.link-offset-2-hover:hover { + text-underline-offset: 0.25em !important; +} + +.link-offset-3 { + text-underline-offset: 0.375em !important; +} + +.link-offset-3-hover:hover { + text-underline-offset: 0.375em !important; +} + +.link-underline-primary { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-secondary { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-success { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-info { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-warning { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-danger { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-light { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-dark { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} + +.link-underline-opacity-0 { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-0-hover:hover { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-10 { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-10-hover:hover { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-25 { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-25-hover:hover { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-50 { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-50-hover:hover { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-75 { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-75-hover:hover { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-100 { + --bs-link-underline-opacity: 1; +} + +.link-underline-opacity-100-hover:hover { + --bs-link-underline-opacity: 1; +} + +.bg-primary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-success { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-info { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-warning { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-danger { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-light { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-dark { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-black { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-white { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-body { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-transparent { + --bs-bg-opacity: 1; + background-color: transparent !important; +} + +.bg-body-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-body-tertiary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-opacity-10 { + --bs-bg-opacity: 0.1; +} + +.bg-opacity-25 { + --bs-bg-opacity: 0.25; +} + +.bg-opacity-50 { + --bs-bg-opacity: 0.5; +} + +.bg-opacity-75 { + --bs-bg-opacity: 0.75; +} + +.bg-opacity-100 { + --bs-bg-opacity: 1; +} + +.bg-primary-subtle { + background-color: var(--bs-primary-bg-subtle) !important; +} + +.bg-secondary-subtle { + background-color: var(--bs-secondary-bg-subtle) !important; +} + +.bg-success-subtle { + background-color: var(--bs-success-bg-subtle) !important; +} + +.bg-info-subtle { + background-color: var(--bs-info-bg-subtle) !important; +} + +.bg-warning-subtle { + background-color: var(--bs-warning-bg-subtle) !important; +} + +.bg-danger-subtle { + background-color: var(--bs-danger-bg-subtle) !important; +} + +.bg-light-subtle { + background-color: var(--bs-light-bg-subtle) !important; +} + +.bg-dark-subtle { + background-color: var(--bs-dark-bg-subtle) !important; +} + +.bg-gradient { + background-image: var(--bs-gradient) !important; +} + +.user-select-all { + -webkit-user-select: all !important; + -moz-user-select: all !important; + user-select: all !important; +} + +.user-select-auto { + -webkit-user-select: auto !important; + -moz-user-select: auto !important; + user-select: auto !important; +} + +.user-select-none { + -webkit-user-select: none !important; + -moz-user-select: none !important; + user-select: none !important; +} + +.pe-none { + pointer-events: none !important; +} + +.pe-auto { + pointer-events: auto !important; +} + +.rounded { + border-radius: var(--bs-border-radius) !important; +} + +.rounded-0 { + border-radius: 0 !important; +} + +.rounded-1 { + border-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-2 { + border-radius: var(--bs-border-radius) !important; +} + +.rounded-3 { + border-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-4 { + border-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-5 { + border-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.rounded-pill { + border-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-top { + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-0 { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +.rounded-top-1 { + border-top-left-radius: var(--bs-border-radius-sm) !important; + border-top-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-top-2 { + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-3 { + border-top-left-radius: var(--bs-border-radius-lg) !important; + border-top-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-top-4 { + border-top-left-radius: var(--bs-border-radius-xl) !important; + border-top-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-top-5 { + border-top-left-radius: var(--bs-border-radius-xxl) !important; + border-top-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-top-circle { + border-top-left-radius: 50% !important; + border-top-right-radius: 50% !important; +} + +.rounded-top-pill { + border-top-left-radius: var(--bs-border-radius-pill) !important; + border-top-right-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-end { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-0 { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.rounded-end-1 { + border-top-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-end-2 { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-3 { + border-top-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-end-4 { + border-top-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-end-5 { + border-top-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-end-circle { + border-top-right-radius: 50% !important; + border-bottom-right-radius: 50% !important; +} + +.rounded-end-pill { + border-top-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-right-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-bottom { + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-0 { + border-bottom-right-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.rounded-bottom-1 { + border-bottom-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-bottom-2 { + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-3 { + border-bottom-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-bottom-4 { + border-bottom-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-bottom-5 { + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-bottom-circle { + border-bottom-right-radius: 50% !important; + border-bottom-left-radius: 50% !important; +} + +.rounded-bottom-pill { + border-bottom-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-left-radius: var(--bs-border-radius-pill) !important; +} + +.rounded-start { + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-0 { + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; +} + +.rounded-start-1 { + border-bottom-left-radius: var(--bs-border-radius-sm) !important; + border-top-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-start-2 { + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-3 { + border-bottom-left-radius: var(--bs-border-radius-lg) !important; + border-top-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-start-4 { + border-bottom-left-radius: var(--bs-border-radius-xl) !important; + border-top-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-start-5 { + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; + border-top-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-start-circle { + border-bottom-left-radius: 50% !important; + border-top-left-radius: 50% !important; +} + +.rounded-start-pill { + border-bottom-left-radius: var(--bs-border-radius-pill) !important; + border-top-left-radius: var(--bs-border-radius-pill) !important; +} + +.visible { + visibility: visible !important; +} + +.invisible { + visibility: hidden !important; +} + +.z-n1 { + z-index: -1 !important; +} + +.z-0 { + z-index: 0 !important; +} + +.z-1 { + z-index: 1 !important; +} + +.z-2 { + z-index: 2 !important; +} + +.z-3 { + z-index: 3 !important; +} + +@media (min-width: 576px) { + .float-sm-start { + float: left !important; + } + .float-sm-end { + float: right !important; + } + .float-sm-none { + float: none !important; + } + .object-fit-sm-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-sm-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-sm-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-sm-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-sm-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-sm-inline { + display: inline !important; + } + .d-sm-inline-block { + display: inline-block !important; + } + .d-sm-block { + display: block !important; + } + .d-sm-grid { + display: grid !important; + } + .d-sm-inline-grid { + display: inline-grid !important; + } + .d-sm-table { + display: table !important; + } + .d-sm-table-row { + display: table-row !important; + } + .d-sm-table-cell { + display: table-cell !important; + } + .d-sm-flex { + display: flex !important; + } + .d-sm-inline-flex { + display: inline-flex !important; + } + .d-sm-none { + display: none !important; + } + .flex-sm-fill { + flex: 1 1 auto !important; + } + .flex-sm-row { + flex-direction: row !important; + } + .flex-sm-column { + flex-direction: column !important; + } + .flex-sm-row-reverse { + flex-direction: row-reverse !important; + } + .flex-sm-column-reverse { + flex-direction: column-reverse !important; + } + .flex-sm-grow-0 { + flex-grow: 0 !important; + } + .flex-sm-grow-1 { + flex-grow: 1 !important; + } + .flex-sm-shrink-0 { + flex-shrink: 0 !important; + } + .flex-sm-shrink-1 { + flex-shrink: 1 !important; + } + .flex-sm-wrap { + flex-wrap: wrap !important; + } + .flex-sm-nowrap { + flex-wrap: nowrap !important; + } + .flex-sm-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-sm-start { + justify-content: flex-start !important; + } + .justify-content-sm-end { + justify-content: flex-end !important; + } + .justify-content-sm-center { + justify-content: center !important; + } + .justify-content-sm-between { + justify-content: space-between !important; + } + .justify-content-sm-around { + justify-content: space-around !important; + } + .justify-content-sm-evenly { + justify-content: space-evenly !important; + } + .align-items-sm-start { + align-items: flex-start !important; + } + .align-items-sm-end { + align-items: flex-end !important; + } + .align-items-sm-center { + align-items: center !important; + } + .align-items-sm-baseline { + align-items: baseline !important; + } + .align-items-sm-stretch { + align-items: stretch !important; + } + .align-content-sm-start { + align-content: flex-start !important; + } + .align-content-sm-end { + align-content: flex-end !important; + } + .align-content-sm-center { + align-content: center !important; + } + .align-content-sm-between { + align-content: space-between !important; + } + .align-content-sm-around { + align-content: space-around !important; + } + .align-content-sm-stretch { + align-content: stretch !important; + } + .align-self-sm-auto { + align-self: auto !important; + } + .align-self-sm-start { + align-self: flex-start !important; + } + .align-self-sm-end { + align-self: flex-end !important; + } + .align-self-sm-center { + align-self: center !important; + } + .align-self-sm-baseline { + align-self: baseline !important; + } + .align-self-sm-stretch { + align-self: stretch !important; + } + .order-sm-first { + order: -1 !important; + } + .order-sm-0 { + order: 0 !important; + } + .order-sm-1 { + order: 1 !important; + } + .order-sm-2 { + order: 2 !important; + } + .order-sm-3 { + order: 3 !important; + } + .order-sm-4 { + order: 4 !important; + } + .order-sm-5 { + order: 5 !important; + } + .order-sm-last { + order: 6 !important; + } + .m-sm-0 { + margin: 0 !important; + } + .m-sm-1 { + margin: 0.25rem !important; + } + .m-sm-2 { + margin: 0.5rem !important; + } + .m-sm-3 { + margin: 1rem !important; + } + .m-sm-4 { + margin: 1.5rem !important; + } + .m-sm-5 { + margin: 3rem !important; + } + .m-sm-auto { + margin: auto !important; + } + .mx-sm-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-sm-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-sm-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-sm-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-sm-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-sm-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-sm-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-sm-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-sm-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-sm-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-sm-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-sm-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-sm-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-sm-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-sm-0 { + margin-top: 0 !important; + } + .mt-sm-1 { + margin-top: 0.25rem !important; + } + .mt-sm-2 { + margin-top: 0.5rem !important; + } + .mt-sm-3 { + margin-top: 1rem !important; + } + .mt-sm-4 { + margin-top: 1.5rem !important; + } + .mt-sm-5 { + margin-top: 3rem !important; + } + .mt-sm-auto { + margin-top: auto !important; + } + .me-sm-0 { + margin-right: 0 !important; + } + .me-sm-1 { + margin-right: 0.25rem !important; + } + .me-sm-2 { + margin-right: 0.5rem !important; + } + .me-sm-3 { + margin-right: 1rem !important; + } + .me-sm-4 { + margin-right: 1.5rem !important; + } + .me-sm-5 { + margin-right: 3rem !important; + } + .me-sm-auto { + margin-right: auto !important; + } + .mb-sm-0 { + margin-bottom: 0 !important; + } + .mb-sm-1 { + margin-bottom: 0.25rem !important; + } + .mb-sm-2 { + margin-bottom: 0.5rem !important; + } + .mb-sm-3 { + margin-bottom: 1rem !important; + } + .mb-sm-4 { + margin-bottom: 1.5rem !important; + } + .mb-sm-5 { + margin-bottom: 3rem !important; + } + .mb-sm-auto { + margin-bottom: auto !important; + } + .ms-sm-0 { + margin-left: 0 !important; + } + .ms-sm-1 { + margin-left: 0.25rem !important; + } + .ms-sm-2 { + margin-left: 0.5rem !important; + } + .ms-sm-3 { + margin-left: 1rem !important; + } + .ms-sm-4 { + margin-left: 1.5rem !important; + } + .ms-sm-5 { + margin-left: 3rem !important; + } + .ms-sm-auto { + margin-left: auto !important; + } + .p-sm-0 { + padding: 0 !important; + } + .p-sm-1 { + padding: 0.25rem !important; + } + .p-sm-2 { + padding: 0.5rem !important; + } + .p-sm-3 { + padding: 1rem !important; + } + .p-sm-4 { + padding: 1.5rem !important; + } + .p-sm-5 { + padding: 3rem !important; + } + .px-sm-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-sm-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-sm-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-sm-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-sm-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-sm-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-sm-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-sm-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-sm-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-sm-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-sm-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-sm-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-sm-0 { + padding-top: 0 !important; + } + .pt-sm-1 { + padding-top: 0.25rem !important; + } + .pt-sm-2 { + padding-top: 0.5rem !important; + } + .pt-sm-3 { + padding-top: 1rem !important; + } + .pt-sm-4 { + padding-top: 1.5rem !important; + } + .pt-sm-5 { + padding-top: 3rem !important; + } + .pe-sm-0 { + padding-right: 0 !important; + } + .pe-sm-1 { + padding-right: 0.25rem !important; + } + .pe-sm-2 { + padding-right: 0.5rem !important; + } + .pe-sm-3 { + padding-right: 1rem !important; + } + .pe-sm-4 { + padding-right: 1.5rem !important; + } + .pe-sm-5 { + padding-right: 3rem !important; + } + .pb-sm-0 { + padding-bottom: 0 !important; + } + .pb-sm-1 { + padding-bottom: 0.25rem !important; + } + .pb-sm-2 { + padding-bottom: 0.5rem !important; + } + .pb-sm-3 { + padding-bottom: 1rem !important; + } + .pb-sm-4 { + padding-bottom: 1.5rem !important; + } + .pb-sm-5 { + padding-bottom: 3rem !important; + } + .ps-sm-0 { + padding-left: 0 !important; + } + .ps-sm-1 { + padding-left: 0.25rem !important; + } + .ps-sm-2 { + padding-left: 0.5rem !important; + } + .ps-sm-3 { + padding-left: 1rem !important; + } + .ps-sm-4 { + padding-left: 1.5rem !important; + } + .ps-sm-5 { + padding-left: 3rem !important; + } + .gap-sm-0 { + gap: 0 !important; + } + .gap-sm-1 { + gap: 0.25rem !important; + } + .gap-sm-2 { + gap: 0.5rem !important; + } + .gap-sm-3 { + gap: 1rem !important; + } + .gap-sm-4 { + gap: 1.5rem !important; + } + .gap-sm-5 { + gap: 3rem !important; + } + .row-gap-sm-0 { + row-gap: 0 !important; + } + .row-gap-sm-1 { + row-gap: 0.25rem !important; + } + .row-gap-sm-2 { + row-gap: 0.5rem !important; + } + .row-gap-sm-3 { + row-gap: 1rem !important; + } + .row-gap-sm-4 { + row-gap: 1.5rem !important; + } + .row-gap-sm-5 { + row-gap: 3rem !important; + } + .column-gap-sm-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-sm-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-sm-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-sm-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-sm-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-sm-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-sm-start { + text-align: left !important; + } + .text-sm-end { + text-align: right !important; + } + .text-sm-center { + text-align: center !important; + } +} +@media (min-width: 768px) { + .float-md-start { + float: left !important; + } + .float-md-end { + float: right !important; + } + .float-md-none { + float: none !important; + } + .object-fit-md-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-md-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-md-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-md-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-md-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-md-inline { + display: inline !important; + } + .d-md-inline-block { + display: inline-block !important; + } + .d-md-block { + display: block !important; + } + .d-md-grid { + display: grid !important; + } + .d-md-inline-grid { + display: inline-grid !important; + } + .d-md-table { + display: table !important; + } + .d-md-table-row { + display: table-row !important; + } + .d-md-table-cell { + display: table-cell !important; + } + .d-md-flex { + display: flex !important; + } + .d-md-inline-flex { + display: inline-flex !important; + } + .d-md-none { + display: none !important; + } + .flex-md-fill { + flex: 1 1 auto !important; + } + .flex-md-row { + flex-direction: row !important; + } + .flex-md-column { + flex-direction: column !important; + } + .flex-md-row-reverse { + flex-direction: row-reverse !important; + } + .flex-md-column-reverse { + flex-direction: column-reverse !important; + } + .flex-md-grow-0 { + flex-grow: 0 !important; + } + .flex-md-grow-1 { + flex-grow: 1 !important; + } + .flex-md-shrink-0 { + flex-shrink: 0 !important; + } + .flex-md-shrink-1 { + flex-shrink: 1 !important; + } + .flex-md-wrap { + flex-wrap: wrap !important; + } + .flex-md-nowrap { + flex-wrap: nowrap !important; + } + .flex-md-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-md-start { + justify-content: flex-start !important; + } + .justify-content-md-end { + justify-content: flex-end !important; + } + .justify-content-md-center { + justify-content: center !important; + } + .justify-content-md-between { + justify-content: space-between !important; + } + .justify-content-md-around { + justify-content: space-around !important; + } + .justify-content-md-evenly { + justify-content: space-evenly !important; + } + .align-items-md-start { + align-items: flex-start !important; + } + .align-items-md-end { + align-items: flex-end !important; + } + .align-items-md-center { + align-items: center !important; + } + .align-items-md-baseline { + align-items: baseline !important; + } + .align-items-md-stretch { + align-items: stretch !important; + } + .align-content-md-start { + align-content: flex-start !important; + } + .align-content-md-end { + align-content: flex-end !important; + } + .align-content-md-center { + align-content: center !important; + } + .align-content-md-between { + align-content: space-between !important; + } + .align-content-md-around { + align-content: space-around !important; + } + .align-content-md-stretch { + align-content: stretch !important; + } + .align-self-md-auto { + align-self: auto !important; + } + .align-self-md-start { + align-self: flex-start !important; + } + .align-self-md-end { + align-self: flex-end !important; + } + .align-self-md-center { + align-self: center !important; + } + .align-self-md-baseline { + align-self: baseline !important; + } + .align-self-md-stretch { + align-self: stretch !important; + } + .order-md-first { + order: -1 !important; + } + .order-md-0 { + order: 0 !important; + } + .order-md-1 { + order: 1 !important; + } + .order-md-2 { + order: 2 !important; + } + .order-md-3 { + order: 3 !important; + } + .order-md-4 { + order: 4 !important; + } + .order-md-5 { + order: 5 !important; + } + .order-md-last { + order: 6 !important; + } + .m-md-0 { + margin: 0 !important; + } + .m-md-1 { + margin: 0.25rem !important; + } + .m-md-2 { + margin: 0.5rem !important; + } + .m-md-3 { + margin: 1rem !important; + } + .m-md-4 { + margin: 1.5rem !important; + } + .m-md-5 { + margin: 3rem !important; + } + .m-md-auto { + margin: auto !important; + } + .mx-md-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-md-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-md-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-md-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-md-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-md-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-md-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-md-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-md-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-md-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-md-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-md-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-md-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-md-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-md-0 { + margin-top: 0 !important; + } + .mt-md-1 { + margin-top: 0.25rem !important; + } + .mt-md-2 { + margin-top: 0.5rem !important; + } + .mt-md-3 { + margin-top: 1rem !important; + } + .mt-md-4 { + margin-top: 1.5rem !important; + } + .mt-md-5 { + margin-top: 3rem !important; + } + .mt-md-auto { + margin-top: auto !important; + } + .me-md-0 { + margin-right: 0 !important; + } + .me-md-1 { + margin-right: 0.25rem !important; + } + .me-md-2 { + margin-right: 0.5rem !important; + } + .me-md-3 { + margin-right: 1rem !important; + } + .me-md-4 { + margin-right: 1.5rem !important; + } + .me-md-5 { + margin-right: 3rem !important; + } + .me-md-auto { + margin-right: auto !important; + } + .mb-md-0 { + margin-bottom: 0 !important; + } + .mb-md-1 { + margin-bottom: 0.25rem !important; + } + .mb-md-2 { + margin-bottom: 0.5rem !important; + } + .mb-md-3 { + margin-bottom: 1rem !important; + } + .mb-md-4 { + margin-bottom: 1.5rem !important; + } + .mb-md-5 { + margin-bottom: 3rem !important; + } + .mb-md-auto { + margin-bottom: auto !important; + } + .ms-md-0 { + margin-left: 0 !important; + } + .ms-md-1 { + margin-left: 0.25rem !important; + } + .ms-md-2 { + margin-left: 0.5rem !important; + } + .ms-md-3 { + margin-left: 1rem !important; + } + .ms-md-4 { + margin-left: 1.5rem !important; + } + .ms-md-5 { + margin-left: 3rem !important; + } + .ms-md-auto { + margin-left: auto !important; + } + .p-md-0 { + padding: 0 !important; + } + .p-md-1 { + padding: 0.25rem !important; + } + .p-md-2 { + padding: 0.5rem !important; + } + .p-md-3 { + padding: 1rem !important; + } + .p-md-4 { + padding: 1.5rem !important; + } + .p-md-5 { + padding: 3rem !important; + } + .px-md-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-md-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-md-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-md-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-md-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-md-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-md-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-md-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-md-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-md-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-md-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-md-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-md-0 { + padding-top: 0 !important; + } + .pt-md-1 { + padding-top: 0.25rem !important; + } + .pt-md-2 { + padding-top: 0.5rem !important; + } + .pt-md-3 { + padding-top: 1rem !important; + } + .pt-md-4 { + padding-top: 1.5rem !important; + } + .pt-md-5 { + padding-top: 3rem !important; + } + .pe-md-0 { + padding-right: 0 !important; + } + .pe-md-1 { + padding-right: 0.25rem !important; + } + .pe-md-2 { + padding-right: 0.5rem !important; + } + .pe-md-3 { + padding-right: 1rem !important; + } + .pe-md-4 { + padding-right: 1.5rem !important; + } + .pe-md-5 { + padding-right: 3rem !important; + } + .pb-md-0 { + padding-bottom: 0 !important; + } + .pb-md-1 { + padding-bottom: 0.25rem !important; + } + .pb-md-2 { + padding-bottom: 0.5rem !important; + } + .pb-md-3 { + padding-bottom: 1rem !important; + } + .pb-md-4 { + padding-bottom: 1.5rem !important; + } + .pb-md-5 { + padding-bottom: 3rem !important; + } + .ps-md-0 { + padding-left: 0 !important; + } + .ps-md-1 { + padding-left: 0.25rem !important; + } + .ps-md-2 { + padding-left: 0.5rem !important; + } + .ps-md-3 { + padding-left: 1rem !important; + } + .ps-md-4 { + padding-left: 1.5rem !important; + } + .ps-md-5 { + padding-left: 3rem !important; + } + .gap-md-0 { + gap: 0 !important; + } + .gap-md-1 { + gap: 0.25rem !important; + } + .gap-md-2 { + gap: 0.5rem !important; + } + .gap-md-3 { + gap: 1rem !important; + } + .gap-md-4 { + gap: 1.5rem !important; + } + .gap-md-5 { + gap: 3rem !important; + } + .row-gap-md-0 { + row-gap: 0 !important; + } + .row-gap-md-1 { + row-gap: 0.25rem !important; + } + .row-gap-md-2 { + row-gap: 0.5rem !important; + } + .row-gap-md-3 { + row-gap: 1rem !important; + } + .row-gap-md-4 { + row-gap: 1.5rem !important; + } + .row-gap-md-5 { + row-gap: 3rem !important; + } + .column-gap-md-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-md-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-md-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-md-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-md-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-md-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-md-start { + text-align: left !important; + } + .text-md-end { + text-align: right !important; + } + .text-md-center { + text-align: center !important; + } +} +@media (min-width: 992px) { + .float-lg-start { + float: left !important; + } + .float-lg-end { + float: right !important; + } + .float-lg-none { + float: none !important; + } + .object-fit-lg-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-lg-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-lg-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-lg-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-lg-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-lg-inline { + display: inline !important; + } + .d-lg-inline-block { + display: inline-block !important; + } + .d-lg-block { + display: block !important; + } + .d-lg-grid { + display: grid !important; + } + .d-lg-inline-grid { + display: inline-grid !important; + } + .d-lg-table { + display: table !important; + } + .d-lg-table-row { + display: table-row !important; + } + .d-lg-table-cell { + display: table-cell !important; + } + .d-lg-flex { + display: flex !important; + } + .d-lg-inline-flex { + display: inline-flex !important; + } + .d-lg-none { + display: none !important; + } + .flex-lg-fill { + flex: 1 1 auto !important; + } + .flex-lg-row { + flex-direction: row !important; + } + .flex-lg-column { + flex-direction: column !important; + } + .flex-lg-row-reverse { + flex-direction: row-reverse !important; + } + .flex-lg-column-reverse { + flex-direction: column-reverse !important; + } + .flex-lg-grow-0 { + flex-grow: 0 !important; + } + .flex-lg-grow-1 { + flex-grow: 1 !important; + } + .flex-lg-shrink-0 { + flex-shrink: 0 !important; + } + .flex-lg-shrink-1 { + flex-shrink: 1 !important; + } + .flex-lg-wrap { + flex-wrap: wrap !important; + } + .flex-lg-nowrap { + flex-wrap: nowrap !important; + } + .flex-lg-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-lg-start { + justify-content: flex-start !important; + } + .justify-content-lg-end { + justify-content: flex-end !important; + } + .justify-content-lg-center { + justify-content: center !important; + } + .justify-content-lg-between { + justify-content: space-between !important; + } + .justify-content-lg-around { + justify-content: space-around !important; + } + .justify-content-lg-evenly { + justify-content: space-evenly !important; + } + .align-items-lg-start { + align-items: flex-start !important; + } + .align-items-lg-end { + align-items: flex-end !important; + } + .align-items-lg-center { + align-items: center !important; + } + .align-items-lg-baseline { + align-items: baseline !important; + } + .align-items-lg-stretch { + align-items: stretch !important; + } + .align-content-lg-start { + align-content: flex-start !important; + } + .align-content-lg-end { + align-content: flex-end !important; + } + .align-content-lg-center { + align-content: center !important; + } + .align-content-lg-between { + align-content: space-between !important; + } + .align-content-lg-around { + align-content: space-around !important; + } + .align-content-lg-stretch { + align-content: stretch !important; + } + .align-self-lg-auto { + align-self: auto !important; + } + .align-self-lg-start { + align-self: flex-start !important; + } + .align-self-lg-end { + align-self: flex-end !important; + } + .align-self-lg-center { + align-self: center !important; + } + .align-self-lg-baseline { + align-self: baseline !important; + } + .align-self-lg-stretch { + align-self: stretch !important; + } + .order-lg-first { + order: -1 !important; + } + .order-lg-0 { + order: 0 !important; + } + .order-lg-1 { + order: 1 !important; + } + .order-lg-2 { + order: 2 !important; + } + .order-lg-3 { + order: 3 !important; + } + .order-lg-4 { + order: 4 !important; + } + .order-lg-5 { + order: 5 !important; + } + .order-lg-last { + order: 6 !important; + } + .m-lg-0 { + margin: 0 !important; + } + .m-lg-1 { + margin: 0.25rem !important; + } + .m-lg-2 { + margin: 0.5rem !important; + } + .m-lg-3 { + margin: 1rem !important; + } + .m-lg-4 { + margin: 1.5rem !important; + } + .m-lg-5 { + margin: 3rem !important; + } + .m-lg-auto { + margin: auto !important; + } + .mx-lg-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-lg-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-lg-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-lg-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-lg-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-lg-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-lg-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-lg-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-lg-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-lg-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-lg-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-lg-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-lg-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-lg-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-lg-0 { + margin-top: 0 !important; + } + .mt-lg-1 { + margin-top: 0.25rem !important; + } + .mt-lg-2 { + margin-top: 0.5rem !important; + } + .mt-lg-3 { + margin-top: 1rem !important; + } + .mt-lg-4 { + margin-top: 1.5rem !important; + } + .mt-lg-5 { + margin-top: 3rem !important; + } + .mt-lg-auto { + margin-top: auto !important; + } + .me-lg-0 { + margin-right: 0 !important; + } + .me-lg-1 { + margin-right: 0.25rem !important; + } + .me-lg-2 { + margin-right: 0.5rem !important; + } + .me-lg-3 { + margin-right: 1rem !important; + } + .me-lg-4 { + margin-right: 1.5rem !important; + } + .me-lg-5 { + margin-right: 3rem !important; + } + .me-lg-auto { + margin-right: auto !important; + } + .mb-lg-0 { + margin-bottom: 0 !important; + } + .mb-lg-1 { + margin-bottom: 0.25rem !important; + } + .mb-lg-2 { + margin-bottom: 0.5rem !important; + } + .mb-lg-3 { + margin-bottom: 1rem !important; + } + .mb-lg-4 { + margin-bottom: 1.5rem !important; + } + .mb-lg-5 { + margin-bottom: 3rem !important; + } + .mb-lg-auto { + margin-bottom: auto !important; + } + .ms-lg-0 { + margin-left: 0 !important; + } + .ms-lg-1 { + margin-left: 0.25rem !important; + } + .ms-lg-2 { + margin-left: 0.5rem !important; + } + .ms-lg-3 { + margin-left: 1rem !important; + } + .ms-lg-4 { + margin-left: 1.5rem !important; + } + .ms-lg-5 { + margin-left: 3rem !important; + } + .ms-lg-auto { + margin-left: auto !important; + } + .p-lg-0 { + padding: 0 !important; + } + .p-lg-1 { + padding: 0.25rem !important; + } + .p-lg-2 { + padding: 0.5rem !important; + } + .p-lg-3 { + padding: 1rem !important; + } + .p-lg-4 { + padding: 1.5rem !important; + } + .p-lg-5 { + padding: 3rem !important; + } + .px-lg-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-lg-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-lg-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-lg-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-lg-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-lg-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-lg-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-lg-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-lg-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-lg-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-lg-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-lg-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-lg-0 { + padding-top: 0 !important; + } + .pt-lg-1 { + padding-top: 0.25rem !important; + } + .pt-lg-2 { + padding-top: 0.5rem !important; + } + .pt-lg-3 { + padding-top: 1rem !important; + } + .pt-lg-4 { + padding-top: 1.5rem !important; + } + .pt-lg-5 { + padding-top: 3rem !important; + } + .pe-lg-0 { + padding-right: 0 !important; + } + .pe-lg-1 { + padding-right: 0.25rem !important; + } + .pe-lg-2 { + padding-right: 0.5rem !important; + } + .pe-lg-3 { + padding-right: 1rem !important; + } + .pe-lg-4 { + padding-right: 1.5rem !important; + } + .pe-lg-5 { + padding-right: 3rem !important; + } + .pb-lg-0 { + padding-bottom: 0 !important; + } + .pb-lg-1 { + padding-bottom: 0.25rem !important; + } + .pb-lg-2 { + padding-bottom: 0.5rem !important; + } + .pb-lg-3 { + padding-bottom: 1rem !important; + } + .pb-lg-4 { + padding-bottom: 1.5rem !important; + } + .pb-lg-5 { + padding-bottom: 3rem !important; + } + .ps-lg-0 { + padding-left: 0 !important; + } + .ps-lg-1 { + padding-left: 0.25rem !important; + } + .ps-lg-2 { + padding-left: 0.5rem !important; + } + .ps-lg-3 { + padding-left: 1rem !important; + } + .ps-lg-4 { + padding-left: 1.5rem !important; + } + .ps-lg-5 { + padding-left: 3rem !important; + } + .gap-lg-0 { + gap: 0 !important; + } + .gap-lg-1 { + gap: 0.25rem !important; + } + .gap-lg-2 { + gap: 0.5rem !important; + } + .gap-lg-3 { + gap: 1rem !important; + } + .gap-lg-4 { + gap: 1.5rem !important; + } + .gap-lg-5 { + gap: 3rem !important; + } + .row-gap-lg-0 { + row-gap: 0 !important; + } + .row-gap-lg-1 { + row-gap: 0.25rem !important; + } + .row-gap-lg-2 { + row-gap: 0.5rem !important; + } + .row-gap-lg-3 { + row-gap: 1rem !important; + } + .row-gap-lg-4 { + row-gap: 1.5rem !important; + } + .row-gap-lg-5 { + row-gap: 3rem !important; + } + .column-gap-lg-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-lg-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-lg-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-lg-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-lg-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-lg-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-lg-start { + text-align: left !important; + } + .text-lg-end { + text-align: right !important; + } + .text-lg-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .float-xl-start { + float: left !important; + } + .float-xl-end { + float: right !important; + } + .float-xl-none { + float: none !important; + } + .object-fit-xl-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-xl-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-xl-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-xl-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-xl-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-xl-inline { + display: inline !important; + } + .d-xl-inline-block { + display: inline-block !important; + } + .d-xl-block { + display: block !important; + } + .d-xl-grid { + display: grid !important; + } + .d-xl-inline-grid { + display: inline-grid !important; + } + .d-xl-table { + display: table !important; + } + .d-xl-table-row { + display: table-row !important; + } + .d-xl-table-cell { + display: table-cell !important; + } + .d-xl-flex { + display: flex !important; + } + .d-xl-inline-flex { + display: inline-flex !important; + } + .d-xl-none { + display: none !important; + } + .flex-xl-fill { + flex: 1 1 auto !important; + } + .flex-xl-row { + flex-direction: row !important; + } + .flex-xl-column { + flex-direction: column !important; + } + .flex-xl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xl-grow-0 { + flex-grow: 0 !important; + } + .flex-xl-grow-1 { + flex-grow: 1 !important; + } + .flex-xl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xl-wrap { + flex-wrap: wrap !important; + } + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xl-start { + justify-content: flex-start !important; + } + .justify-content-xl-end { + justify-content: flex-end !important; + } + .justify-content-xl-center { + justify-content: center !important; + } + .justify-content-xl-between { + justify-content: space-between !important; + } + .justify-content-xl-around { + justify-content: space-around !important; + } + .justify-content-xl-evenly { + justify-content: space-evenly !important; + } + .align-items-xl-start { + align-items: flex-start !important; + } + .align-items-xl-end { + align-items: flex-end !important; + } + .align-items-xl-center { + align-items: center !important; + } + .align-items-xl-baseline { + align-items: baseline !important; + } + .align-items-xl-stretch { + align-items: stretch !important; + } + .align-content-xl-start { + align-content: flex-start !important; + } + .align-content-xl-end { + align-content: flex-end !important; + } + .align-content-xl-center { + align-content: center !important; + } + .align-content-xl-between { + align-content: space-between !important; + } + .align-content-xl-around { + align-content: space-around !important; + } + .align-content-xl-stretch { + align-content: stretch !important; + } + .align-self-xl-auto { + align-self: auto !important; + } + .align-self-xl-start { + align-self: flex-start !important; + } + .align-self-xl-end { + align-self: flex-end !important; + } + .align-self-xl-center { + align-self: center !important; + } + .align-self-xl-baseline { + align-self: baseline !important; + } + .align-self-xl-stretch { + align-self: stretch !important; + } + .order-xl-first { + order: -1 !important; + } + .order-xl-0 { + order: 0 !important; + } + .order-xl-1 { + order: 1 !important; + } + .order-xl-2 { + order: 2 !important; + } + .order-xl-3 { + order: 3 !important; + } + .order-xl-4 { + order: 4 !important; + } + .order-xl-5 { + order: 5 !important; + } + .order-xl-last { + order: 6 !important; + } + .m-xl-0 { + margin: 0 !important; + } + .m-xl-1 { + margin: 0.25rem !important; + } + .m-xl-2 { + margin: 0.5rem !important; + } + .m-xl-3 { + margin: 1rem !important; + } + .m-xl-4 { + margin: 1.5rem !important; + } + .m-xl-5 { + margin: 3rem !important; + } + .m-xl-auto { + margin: auto !important; + } + .mx-xl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xl-0 { + margin-top: 0 !important; + } + .mt-xl-1 { + margin-top: 0.25rem !important; + } + .mt-xl-2 { + margin-top: 0.5rem !important; + } + .mt-xl-3 { + margin-top: 1rem !important; + } + .mt-xl-4 { + margin-top: 1.5rem !important; + } + .mt-xl-5 { + margin-top: 3rem !important; + } + .mt-xl-auto { + margin-top: auto !important; + } + .me-xl-0 { + margin-right: 0 !important; + } + .me-xl-1 { + margin-right: 0.25rem !important; + } + .me-xl-2 { + margin-right: 0.5rem !important; + } + .me-xl-3 { + margin-right: 1rem !important; + } + .me-xl-4 { + margin-right: 1.5rem !important; + } + .me-xl-5 { + margin-right: 3rem !important; + } + .me-xl-auto { + margin-right: auto !important; + } + .mb-xl-0 { + margin-bottom: 0 !important; + } + .mb-xl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xl-3 { + margin-bottom: 1rem !important; + } + .mb-xl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xl-5 { + margin-bottom: 3rem !important; + } + .mb-xl-auto { + margin-bottom: auto !important; + } + .ms-xl-0 { + margin-left: 0 !important; + } + .ms-xl-1 { + margin-left: 0.25rem !important; + } + .ms-xl-2 { + margin-left: 0.5rem !important; + } + .ms-xl-3 { + margin-left: 1rem !important; + } + .ms-xl-4 { + margin-left: 1.5rem !important; + } + .ms-xl-5 { + margin-left: 3rem !important; + } + .ms-xl-auto { + margin-left: auto !important; + } + .p-xl-0 { + padding: 0 !important; + } + .p-xl-1 { + padding: 0.25rem !important; + } + .p-xl-2 { + padding: 0.5rem !important; + } + .p-xl-3 { + padding: 1rem !important; + } + .p-xl-4 { + padding: 1.5rem !important; + } + .p-xl-5 { + padding: 3rem !important; + } + .px-xl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xl-0 { + padding-top: 0 !important; + } + .pt-xl-1 { + padding-top: 0.25rem !important; + } + .pt-xl-2 { + padding-top: 0.5rem !important; + } + .pt-xl-3 { + padding-top: 1rem !important; + } + .pt-xl-4 { + padding-top: 1.5rem !important; + } + .pt-xl-5 { + padding-top: 3rem !important; + } + .pe-xl-0 { + padding-right: 0 !important; + } + .pe-xl-1 { + padding-right: 0.25rem !important; + } + .pe-xl-2 { + padding-right: 0.5rem !important; + } + .pe-xl-3 { + padding-right: 1rem !important; + } + .pe-xl-4 { + padding-right: 1.5rem !important; + } + .pe-xl-5 { + padding-right: 3rem !important; + } + .pb-xl-0 { + padding-bottom: 0 !important; + } + .pb-xl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xl-3 { + padding-bottom: 1rem !important; + } + .pb-xl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xl-5 { + padding-bottom: 3rem !important; + } + .ps-xl-0 { + padding-left: 0 !important; + } + .ps-xl-1 { + padding-left: 0.25rem !important; + } + .ps-xl-2 { + padding-left: 0.5rem !important; + } + .ps-xl-3 { + padding-left: 1rem !important; + } + .ps-xl-4 { + padding-left: 1.5rem !important; + } + .ps-xl-5 { + padding-left: 3rem !important; + } + .gap-xl-0 { + gap: 0 !important; + } + .gap-xl-1 { + gap: 0.25rem !important; + } + .gap-xl-2 { + gap: 0.5rem !important; + } + .gap-xl-3 { + gap: 1rem !important; + } + .gap-xl-4 { + gap: 1.5rem !important; + } + .gap-xl-5 { + gap: 3rem !important; + } + .row-gap-xl-0 { + row-gap: 0 !important; + } + .row-gap-xl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xl-3 { + row-gap: 1rem !important; + } + .row-gap-xl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xl-5 { + row-gap: 3rem !important; + } + .column-gap-xl-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-xl-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-xl-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-xl-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-xl-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-xl-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-xl-start { + text-align: left !important; + } + .text-xl-end { + text-align: right !important; + } + .text-xl-center { + text-align: center !important; + } +} +@media (min-width: 1400px) { + .float-xxl-start { + float: left !important; + } + .float-xxl-end { + float: right !important; + } + .float-xxl-none { + float: none !important; + } + .object-fit-xxl-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-xxl-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-xxl-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-xxl-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-xxl-none { + -o-object-fit: none !important; + object-fit: none !important; + } + .d-xxl-inline { + display: inline !important; + } + .d-xxl-inline-block { + display: inline-block !important; + } + .d-xxl-block { + display: block !important; + } + .d-xxl-grid { + display: grid !important; + } + .d-xxl-inline-grid { + display: inline-grid !important; + } + .d-xxl-table { + display: table !important; + } + .d-xxl-table-row { + display: table-row !important; + } + .d-xxl-table-cell { + display: table-cell !important; + } + .d-xxl-flex { + display: flex !important; + } + .d-xxl-inline-flex { + display: inline-flex !important; + } + .d-xxl-none { + display: none !important; + } + .flex-xxl-fill { + flex: 1 1 auto !important; + } + .flex-xxl-row { + flex-direction: row !important; + } + .flex-xxl-column { + flex-direction: column !important; + } + .flex-xxl-row-reverse { + flex-direction: row-reverse !important; + } + .flex-xxl-column-reverse { + flex-direction: column-reverse !important; + } + .flex-xxl-grow-0 { + flex-grow: 0 !important; + } + .flex-xxl-grow-1 { + flex-grow: 1 !important; + } + .flex-xxl-shrink-0 { + flex-shrink: 0 !important; + } + .flex-xxl-shrink-1 { + flex-shrink: 1 !important; + } + .flex-xxl-wrap { + flex-wrap: wrap !important; + } + .flex-xxl-nowrap { + flex-wrap: nowrap !important; + } + .flex-xxl-wrap-reverse { + flex-wrap: wrap-reverse !important; + } + .justify-content-xxl-start { + justify-content: flex-start !important; + } + .justify-content-xxl-end { + justify-content: flex-end !important; + } + .justify-content-xxl-center { + justify-content: center !important; + } + .justify-content-xxl-between { + justify-content: space-between !important; + } + .justify-content-xxl-around { + justify-content: space-around !important; + } + .justify-content-xxl-evenly { + justify-content: space-evenly !important; + } + .align-items-xxl-start { + align-items: flex-start !important; + } + .align-items-xxl-end { + align-items: flex-end !important; + } + .align-items-xxl-center { + align-items: center !important; + } + .align-items-xxl-baseline { + align-items: baseline !important; + } + .align-items-xxl-stretch { + align-items: stretch !important; + } + .align-content-xxl-start { + align-content: flex-start !important; + } + .align-content-xxl-end { + align-content: flex-end !important; + } + .align-content-xxl-center { + align-content: center !important; + } + .align-content-xxl-between { + align-content: space-between !important; + } + .align-content-xxl-around { + align-content: space-around !important; + } + .align-content-xxl-stretch { + align-content: stretch !important; + } + .align-self-xxl-auto { + align-self: auto !important; + } + .align-self-xxl-start { + align-self: flex-start !important; + } + .align-self-xxl-end { + align-self: flex-end !important; + } + .align-self-xxl-center { + align-self: center !important; + } + .align-self-xxl-baseline { + align-self: baseline !important; + } + .align-self-xxl-stretch { + align-self: stretch !important; + } + .order-xxl-first { + order: -1 !important; + } + .order-xxl-0 { + order: 0 !important; + } + .order-xxl-1 { + order: 1 !important; + } + .order-xxl-2 { + order: 2 !important; + } + .order-xxl-3 { + order: 3 !important; + } + .order-xxl-4 { + order: 4 !important; + } + .order-xxl-5 { + order: 5 !important; + } + .order-xxl-last { + order: 6 !important; + } + .m-xxl-0 { + margin: 0 !important; + } + .m-xxl-1 { + margin: 0.25rem !important; + } + .m-xxl-2 { + margin: 0.5rem !important; + } + .m-xxl-3 { + margin: 1rem !important; + } + .m-xxl-4 { + margin: 1.5rem !important; + } + .m-xxl-5 { + margin: 3rem !important; + } + .m-xxl-auto { + margin: auto !important; + } + .mx-xxl-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mx-xxl-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mx-xxl-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mx-xxl-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mx-xxl-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mx-xxl-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mx-xxl-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .my-xxl-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .my-xxl-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .my-xxl-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .my-xxl-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .my-xxl-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .my-xxl-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .my-xxl-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mt-xxl-0 { + margin-top: 0 !important; + } + .mt-xxl-1 { + margin-top: 0.25rem !important; + } + .mt-xxl-2 { + margin-top: 0.5rem !important; + } + .mt-xxl-3 { + margin-top: 1rem !important; + } + .mt-xxl-4 { + margin-top: 1.5rem !important; + } + .mt-xxl-5 { + margin-top: 3rem !important; + } + .mt-xxl-auto { + margin-top: auto !important; + } + .me-xxl-0 { + margin-right: 0 !important; + } + .me-xxl-1 { + margin-right: 0.25rem !important; + } + .me-xxl-2 { + margin-right: 0.5rem !important; + } + .me-xxl-3 { + margin-right: 1rem !important; + } + .me-xxl-4 { + margin-right: 1.5rem !important; + } + .me-xxl-5 { + margin-right: 3rem !important; + } + .me-xxl-auto { + margin-right: auto !important; + } + .mb-xxl-0 { + margin-bottom: 0 !important; + } + .mb-xxl-1 { + margin-bottom: 0.25rem !important; + } + .mb-xxl-2 { + margin-bottom: 0.5rem !important; + } + .mb-xxl-3 { + margin-bottom: 1rem !important; + } + .mb-xxl-4 { + margin-bottom: 1.5rem !important; + } + .mb-xxl-5 { + margin-bottom: 3rem !important; + } + .mb-xxl-auto { + margin-bottom: auto !important; + } + .ms-xxl-0 { + margin-left: 0 !important; + } + .ms-xxl-1 { + margin-left: 0.25rem !important; + } + .ms-xxl-2 { + margin-left: 0.5rem !important; + } + .ms-xxl-3 { + margin-left: 1rem !important; + } + .ms-xxl-4 { + margin-left: 1.5rem !important; + } + .ms-xxl-5 { + margin-left: 3rem !important; + } + .ms-xxl-auto { + margin-left: auto !important; + } + .p-xxl-0 { + padding: 0 !important; + } + .p-xxl-1 { + padding: 0.25rem !important; + } + .p-xxl-2 { + padding: 0.5rem !important; + } + .p-xxl-3 { + padding: 1rem !important; + } + .p-xxl-4 { + padding: 1.5rem !important; + } + .p-xxl-5 { + padding: 3rem !important; + } + .px-xxl-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .px-xxl-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .px-xxl-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .px-xxl-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .px-xxl-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .px-xxl-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .py-xxl-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .py-xxl-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .py-xxl-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .py-xxl-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .py-xxl-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .py-xxl-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .pt-xxl-0 { + padding-top: 0 !important; + } + .pt-xxl-1 { + padding-top: 0.25rem !important; + } + .pt-xxl-2 { + padding-top: 0.5rem !important; + } + .pt-xxl-3 { + padding-top: 1rem !important; + } + .pt-xxl-4 { + padding-top: 1.5rem !important; + } + .pt-xxl-5 { + padding-top: 3rem !important; + } + .pe-xxl-0 { + padding-right: 0 !important; + } + .pe-xxl-1 { + padding-right: 0.25rem !important; + } + .pe-xxl-2 { + padding-right: 0.5rem !important; + } + .pe-xxl-3 { + padding-right: 1rem !important; + } + .pe-xxl-4 { + padding-right: 1.5rem !important; + } + .pe-xxl-5 { + padding-right: 3rem !important; + } + .pb-xxl-0 { + padding-bottom: 0 !important; + } + .pb-xxl-1 { + padding-bottom: 0.25rem !important; + } + .pb-xxl-2 { + padding-bottom: 0.5rem !important; + } + .pb-xxl-3 { + padding-bottom: 1rem !important; + } + .pb-xxl-4 { + padding-bottom: 1.5rem !important; + } + .pb-xxl-5 { + padding-bottom: 3rem !important; + } + .ps-xxl-0 { + padding-left: 0 !important; + } + .ps-xxl-1 { + padding-left: 0.25rem !important; + } + .ps-xxl-2 { + padding-left: 0.5rem !important; + } + .ps-xxl-3 { + padding-left: 1rem !important; + } + .ps-xxl-4 { + padding-left: 1.5rem !important; + } + .ps-xxl-5 { + padding-left: 3rem !important; + } + .gap-xxl-0 { + gap: 0 !important; + } + .gap-xxl-1 { + gap: 0.25rem !important; + } + .gap-xxl-2 { + gap: 0.5rem !important; + } + .gap-xxl-3 { + gap: 1rem !important; + } + .gap-xxl-4 { + gap: 1.5rem !important; + } + .gap-xxl-5 { + gap: 3rem !important; + } + .row-gap-xxl-0 { + row-gap: 0 !important; + } + .row-gap-xxl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xxl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xxl-3 { + row-gap: 1rem !important; + } + .row-gap-xxl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xxl-5 { + row-gap: 3rem !important; + } + .column-gap-xxl-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-xxl-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-xxl-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-xxl-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-xxl-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-xxl-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } + .text-xxl-start { + text-align: left !important; + } + .text-xxl-end { + text-align: right !important; + } + .text-xxl-center { + text-align: center !important; + } +} +@media (min-width: 1200px) { + .fs-1 { + font-size: 2.5rem !important; + } + .fs-2 { + font-size: 2rem !important; + } + .fs-3 { + font-size: 1.75rem !important; + } + .fs-4 { + font-size: 1.5rem !important; + } +} +@media print { + .d-print-inline { + display: inline !important; + } + .d-print-inline-block { + display: inline-block !important; + } + .d-print-block { + display: block !important; + } + .d-print-grid { + display: grid !important; + } + .d-print-inline-grid { + display: inline-grid !important; + } + .d-print-table { + display: table !important; + } + .d-print-table-row { + display: table-row !important; + } + .d-print-table-cell { + display: table-cell !important; + } + .d-print-flex { + display: flex !important; + } + .d-print-inline-flex { + display: inline-flex !important; + } + .d-print-none { + display: none !important; + } +} + +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/bootstrap/bootstrap.min.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/bootstrap/bootstrap.min.css new file mode 100644 index 00000000..a89937cc --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/bootstrap/bootstrap.min.css @@ -0,0 +1,6 @@ +@charset "UTF-8";/*! + * Bootstrap v5.3.1 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-body-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::-moz-placeholder{color:var(--bs-secondary-color);opacity:1}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-tertiary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-tertiary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:not(:-moz-placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control-plaintext~label::after,.form-floating>.form-control:focus~label::after,.form-floating>.form-control:not(:placeholder-shown)~label::after,.form-floating>.form-select~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>.form-control:-webkit-autofill~label{color:rgba(var(--bs-body-color-rgb),.65);transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.form-floating>.form-control:disabled~label::after,.form-floating>:disabled~label::after{background-color:var(--bs-secondary-bg)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(var(--bs-border-width) * -1);border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(var(--bs-border-width) * -1)}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(var(--bs-border-width) * -1)}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23052c65'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(var(--bs-border-width) * -1)}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;--bs-btn-close-white-filter:invert(1) grayscale(100%) brightness(200%);box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{filter:var(--bs-btn-close-white-filter)}[data-bs-theme=dark] .btn-close{filter:var(--bs-btn-close-white-filter)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}[data-bs-theme=dark] .carousel .carousel-control-next-icon,[data-bs-theme=dark] .carousel .carousel-control-prev-icon,[data-bs-theme=dark].carousel .carousel-control-next-icon,[data-bs-theme=dark].carousel .carousel-control-prev-icon{filter:invert(1) grayscale(100)}[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target],[data-bs-theme=dark].carousel .carousel-indicators [data-bs-target]{background-color:#000}[data-bs-theme=dark] .carousel .carousel-caption,[data-bs-theme=dark].carousel .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css new file mode 100644 index 00000000..229583a8 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css @@ -0,0 +1,1105 @@ +.envelope-viewer-layout { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%); +} + +.envelope-action-bar { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-bottom: 3px solid rgba(126, 34, 206, 0.3); + padding: 1.25rem 2rem; + flex-shrink: 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.envelope-action-bar__inner { + max-width: 1600px; + margin: 0 auto; + display: flex; + align-items: center; + gap: 2rem; +} + +.envelope-logo svg { + filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3)); + color: #7e22ce; +} + +.envelope-title { + font-size: 1.125rem; + font-weight: 700; + color: #1e293b; + letter-spacing: -0.025em; +} + +.envelope-key { + font-size: 0.8125rem; + color: #64748b; + font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; + font-weight: 500; + margin-top: 0.25rem; +} + +.envelope-content { + flex: 1; + min-height: 0; + padding: 1.5rem; + position: relative; + overflow: auto; +} + +.pdf-editor-wrapper { + width: 100%; + height: 100%; +} + +.sender-editor-pdf-viewer { + width: 100%; + height: 100%; +} + +.sender-editor-pdf-viewer .dxbl-toolbar { + justify-content: center; +} + +.sender-editor-pdf-viewer .dxbl-toolbar-left { + margin-inline: auto; +} + +.sender-editor-pdf-viewer .dxbrv-document-surface { + display: flex; + flex-direction: column; + align-items: center; +} + +.sender-editor-pdf-viewer .dxbrv-report-preview-content-flex-item { + width: 100%; + display: flex; + justify-content: center; +} + +.sender-editor-pdf-viewer .dxbrv-report-preview-content { + margin-left: auto; + margin-right: auto; +} + +.pdf-viewer-container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.pdf-thumbnails { + position: relative; + width: 260px; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-radius: 16px 0 0 16px; + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(126, 34, 206, 0.1); + border: 1px solid rgba(126, 34, 206, 0.15); + border-right: none; + display: flex; + flex-direction: column; + overflow: hidden; + flex-shrink: 0; +} + +.pdf-thumbnails__content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.pdf-thumbnails__content::-webkit-scrollbar { + width: 6px; +} + +.pdf-thumbnails__content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.05); + border-radius: 3px; +} + +.pdf-thumbnails__content::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + border-radius: 3px; +} + +.pdf-thumbnails__content::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%); +} + +.pdf-splitter { + width: 4px; + background: transparent; + cursor: col-resize; + flex-shrink: 0; + position: relative; + transition: background 0.2s ease; + z-index: 10; + user-select: none; +} + +.pdf-splitter::before { + content: ''; + position: absolute; + left: -4px; + right: -4px; + top: 0; + bottom: 0; + /* Enlarged hitbox for easier grabbing */ +} + +.pdf-splitter:hover, +.pdf-splitter.resizing { + background: linear-gradient(90deg, + rgba(126, 34, 206, 0.4) 0%, + rgba(42, 82, 152, 0.4) 100%); +} + +.pdf-splitter:active { + background: linear-gradient(90deg, + rgba(126, 34, 206, 0.6) 0%, + rgba(42, 82, 152, 0.6) 100%); +} + +/* Prevent text selection during resize */ +body.resizing { + user-select: none; + cursor: col-resize !important; +} + +.pdf-thumbnail { + cursor: pointer; + border-radius: 8px; + overflow: hidden; + background: white; + border: 2px solid transparent; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.pdf-thumbnail:hover { + border-color: rgba(126, 34, 206, 0.3); + box-shadow: 0 4px 16px rgba(126, 34, 206, 0.2); + transform: translateY(-2px); +} + +.pdf-thumbnail--active { + border-color: #7e22ce; + box-shadow: + 0 4px 16px rgba(126, 34, 206, 0.3), + 0 0 0 3px rgba(126, 34, 206, 0.1); +} + +.pdf-thumbnail__preview { + width: 100%; + aspect-ratio: 210 / 297; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.pdf-thumbnail__canvas { + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; +} + +.pdf-thumbnail__label { + padding: 0.5rem; + text-align: center; + font-size: 0.75rem; + font-weight: 600; + color: #64748b; + background: rgba(126, 34, 206, 0.03); + border-top: 1px solid rgba(126, 34, 206, 0.1); +} + +.pdf-thumbnail--active .pdf-thumbnail__label { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + color: #7e22ce; + font-weight: 700; +} + +.pdf-toolbar__btn--toggle { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.08) 0%, rgba(42, 82, 152, 0.08) 100%); + border-color: rgba(126, 34, 206, 0.25); +} + +.pdf-toolbar__btn--toggle:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.15) 0%, rgba(42, 82, 152, 0.15) 100%); + border-color: rgba(126, 34, 206, 0.5); +} + +.pdf-toolbar { + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-radius: 12px; + padding: 0.75rem 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(126, 34, 206, 0.1); + border: 1px solid rgba(126, 34, 206, 0.15); + flex-shrink: 0; + width: 95%; +} + +.pdf-toolbar__section { + display: flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} + +.pdf-toolbar__zoom-section { + gap: 0.75rem; + flex: 1; + max-width: 400px; + min-width: 280px; + justify-content: center; +} + +.pdf-toolbar__divider { + width: 1px; + height: 32px; + background: linear-gradient(180deg, transparent 0%, rgba(126, 34, 206, 0.2) 50%, transparent 100%); +} + +.pdf-toolbar__btn { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + border: 1px solid rgba(126, 34, 206, 0.2); + border-radius: 8px; + padding: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + color: #1e293b; + min-width: 34px; + min-height: 34px; +} + +.pdf-toolbar__btn:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + border-color: rgba(126, 34, 206, 0.4); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2); +} + +.pdf-toolbar__btn:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(126, 34, 206, 0.15); +} + +.pdf-toolbar__btn:disabled { + opacity: 0.4; + cursor: not-allowed; + background: rgba(0, 0, 0, 0.02); + border-color: rgba(0, 0, 0, 0.1); +} + +.pdf-toolbar__btn--preset { + padding: 0.5rem 0.875rem; + font-size: 0.813rem; + font-weight: 600; + color: #475569; + min-width: auto; + white-space: nowrap; +} + +.pdf-toolbar__btn--preset svg { + flex-shrink: 0; +} + +.pdf-toolbar__page-input-group { + display: flex; + align-items: center; + gap: 0.375rem; + background: white; + border: 1px solid rgba(126, 34, 206, 0.2); + border-radius: 8px; + padding: 0.25rem 0.625rem; +} + +.pdf-toolbar__page-input { + width: 48px; + border: none; + outline: none; + text-align: center; + font-size: 0.875rem; + font-weight: 600; + color: #1e293b; + background: transparent; + -moz-appearance: textfield; +} + +.pdf-toolbar__page-input::-webkit-outer-spin-button, +.pdf-toolbar__page-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.pdf-toolbar__page-total { + font-size: 0.875rem; + font-weight: 500; + color: #64748b; +} + +.pdf-toolbar__zoom-slider-container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; +} + +.pdf-toolbar__zoom-slider { + -webkit-appearance: none; + width: 100%; + min-width: 180px; + max-width: 350px; + height: 6px; + border-radius: 3px; + background: linear-gradient(90deg, + rgba(126, 34, 206, 0.1) 0%, + rgba(126, 34, 206, 0.2) 50%, + rgba(126, 34, 206, 0.1) 100%); + outline: none; + cursor: pointer; +} + +.pdf-toolbar__zoom-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + cursor: pointer; + box-shadow: 0 2px 8px rgba(126, 34, 206, 0.3); + transition: all 0.2s ease; +} + +.pdf-toolbar__zoom-slider::-webkit-slider-thumb:hover { + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(126, 34, 206, 0.4); +} + +.pdf-toolbar__zoom-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + cursor: pointer; + border: none; + box-shadow: 0 2px 8px rgba(126, 34, 206, 0.3); + transition: all 0.2s ease; +} + +.pdf-toolbar__zoom-slider::-moz-range-thumb:hover { + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(126, 34, 206, 0.4); +} + +.pdf-toolbar__zoom-label { + font-size: 0.75rem; + font-weight: 700; + color: #7e22ce; + letter-spacing: 0.025em; + min-width: 45px; + text-align: center; +} + +/* Signature Navigation Styles */ +.pdf-toolbar__signature-nav { + display: flex; + align-items: center; + gap: 0.375rem; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + border: 1px solid rgba(126, 34, 206, 0.2); + border-radius: 10px; + padding: 0.25rem 0.5rem; + flex-shrink: 0; +} + +.pdf-toolbar__btn--signature-nav { + min-width: 30px; + min-height: 30px; + padding: 0.25rem; + background: white; + border: 1px solid rgba(126, 34, 206, 0.25); +} + +.pdf-toolbar__btn--signature-nav:hover:not(:disabled) { + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + border-color: transparent; +} + +.pdf-toolbar__btn--signature-nav:hover:not(:disabled) svg { + color: white; +} + +.pdf-toolbar__signature-counter { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0 0.375rem; +} + +.pdf-toolbar__signature-counter svg { + color: #7e22ce; + flex-shrink: 0; +} + +.pdf-toolbar__signature-counter-text { + font-size: 0.8125rem; + font-weight: 600; + color: #1e293b; + white-space: nowrap; +} + +.pdf-toolbar__signature-badge { + font-size: 0.625rem; + font-weight: 700; + padding: 0.1875rem 0.5rem; + border-radius: 5px; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + color: #7e22ce; + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.pdf-toolbar__signature-badge--complete { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%); + color: #059669; +} + +/* Reset Button Styles */ +.pdf-toolbar__btn--reset { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%); + border-color: rgba(239, 68, 68, 0.3); + color: #dc2626; +} + +.pdf-toolbar__btn--reset:hover:not(:disabled) { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + border-color: transparent; + color: white; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); +} + +.pdf-toolbar__btn--reset svg { + transition: color 0.2s ease; +} + +.pdf-toolbar__btn--reset:hover:not(:disabled) svg { + color: white; +} + +/* Success Button Styles (Signature Created) */ +.pdf-toolbar__btn--success { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%); + border-color: rgba(16, 185, 129, 0.3); + color: #059669; +} + +.pdf-toolbar__btn--success:hover:not(:disabled) { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + border-color: transparent; + color: white; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); +} + +.pdf-toolbar__btn--success svg { + transition: color 0.2s ease; +} + +.pdf-toolbar__btn--success:hover:not(:disabled) svg { + color: white; +} + +/* Signature Change Button */ +.pdf-toolbar__btn--signature-change { + display: flex; + align-items: center; + gap: 0.375rem; + min-width: auto; + padding: 0.5rem 0.75rem; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + border: 1px solid rgba(126, 34, 206, 0.2); + color: #7e22ce; +} + +.pdf-toolbar__btn--signature-change:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + border-color: rgba(126, 34, 206, 0.4); +} + +.pdf-toolbar__btn--signature-change-active { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(5, 150, 105, 0.08) 100%); + border-color: rgba(16, 185, 129, 0.25); + color: #059669; +} + +.pdf-toolbar__btn--signature-change-active:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%); + border-color: rgba(16, 185, 129, 0.35); +} + +.pdf-toolbar__btn--signature-change:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.pdf-toolbar__btn-text { + font-size: 0.813rem; + font-weight: 600; + white-space: nowrap; +} + +.sender-toolbar-action-btn { + min-width: auto; + padding: 0.5rem 0.75rem; +} + +.sender-toolbar-action-btn--compact { + padding: 0.45rem 0.7rem; +} + +.sender-receivers-panel { + display: flex; + flex-direction: column; + gap: 0.625rem; + padding: 0.75rem 0.9rem; + border-radius: 12px; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + border: 1px solid rgba(126, 34, 206, 0.12); +} + +.sender-receivers-panel__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + flex-wrap: wrap; +} + +.sender-receivers-panel__title-wrap { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.sender-receivers-panel__title { + font-size: 0.8rem; + font-weight: 700; + color: #4c1d95; + letter-spacing: 0.02em; +} + +.sender-receivers-panel__count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.5rem; + min-height: 1.5rem; + padding: 0 0.45rem; + border-radius: 999px; + background: rgba(79, 70, 229, 0.12); + color: #5b21b6; + font-size: 0.72rem; + font-weight: 700; +} + +.sender-receivers-panel__add-btn .dxbl-btn { + border-radius: 8px; + font-weight: 600; +} + +.pdf-toolbar-like-btn .dxbl-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + min-height: 34px; + padding: 0.45rem 0.85rem; + border-radius: 8px; + border: 1px solid rgba(126, 34, 206, 0.2); + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + color: #1e293b; + font-size: 0.75rem; + font-weight: 600; + box-shadow: none; + transition: all 0.2s ease; +} + +.pdf-toolbar-like-btn .dxbl-btn:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + border-color: rgba(126, 34, 206, 0.4); + color: #1e293b; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2); +} + +.pdf-toolbar-like-btn .dxbl-btn:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(126, 34, 206, 0.15); +} + +.pdf-toolbar-like-btn--add .dxbl-btn::before, +.pdf-toolbar-like-btn--signature .dxbl-btn::before { + display: inline-block; + font-size: 0.9rem; + line-height: 1; + font-weight: 700; +} + +.pdf-toolbar-like-btn--add .dxbl-btn::before { + content: '+'; + color: #7e22ce; +} + +.pdf-toolbar-like-btn--signature .dxbl-btn { + color: #7e22ce; +} + +.pdf-toolbar-like-btn--signature .dxbl-btn::before { + content: '?'; + color: #7e22ce; +} + +.pdf-toolbar-like-btn--signature .dxbl-btn:hover:not(:disabled) { + color: #7e22ce; +} + +.sender-receivers-panel__empty { + font-size: 0.78rem; + color: #64748b; +} + +.sender-receivers-list { + display: flex; + flex-wrap: wrap; + gap: 0.625rem; +} + +.sender-receiver-chip { + display: inline-flex; + align-items: center; + gap: 0.75rem; + min-width: 220px; + max-width: 100%; + padding: 0.625rem 0.75rem; + border-radius: 12px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(126, 34, 206, 0.12); + box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06); +} + +.sender-receiver-chip__body { + min-width: 0; + flex: 1; +} + +.sender-receiver-chip__name { + font-size: 0.78rem; + font-weight: 700; + color: #1f2937; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sender-receiver-chip__email { + margin-top: 0.15rem; + font-size: 0.72rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sender-receiver-chip__phone { + margin-top: 0.15rem; + font-size: 0.72rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.sender-receiver-popup .dxbl-modal { + border-radius: 18px; +} + +.sender-receiver-popup .dxbl-popup { + max-width: min(720px, calc(100vw - 2rem)); +} + +.sender-receiver-popup .dxbl-popup-content { + padding: 1rem 1.25rem 1.1rem; +} + +.sender-receiver-popup .dxbl-popup-header { + padding: 0.95rem 1.25rem; +} + +.sender-receiver-popup .dxbl-popup-footer { + padding: 0.75rem 1.25rem 1rem; +} + +.sender-receiver-popup__body { + display: flex; + flex-direction: column; + gap: 0.9rem; +} + +.sender-receiver-popup__form-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 1rem 1.25rem; + align-items: start; + min-height: 250px; +} + +.sender-receiver-popup__field { + min-width: 0; +} + +.sender-receiver-popup__label { + margin-bottom: 0.45rem; + font-size: 0.82rem; + font-weight: 600; + color: #475569; +} + +.sender-receiver-popup__field .dxbl-text-edit, +.sender-receiver-popup__field .dxbl-dropdown-edit { + width: 100%; +} + +.sender-receiver-popup__field .dxbl-input-editor, +.sender-receiver-popup__field .dxbl-text-edit-input { + min-height: 38px; +} + +.sender-receiver-popup__suggestions-shell { + min-height: 188px; + margin-top: 0.5rem; +} + +.sender-receiver-popup__suggestions { + border: 1px solid rgba(126, 34, 206, 0.12); + border-radius: 10px; + background: rgba(255, 255, 255, 0.98); + overflow: hidden; +} + +.sender-receiver-popup__suggestions .dxbl-listbox { + border: none; +} + +.sender-receiver-popup__suggestions .dxbl-listbox-scroll-viewer { + max-height: 180px; +} + +.sender-receiver-popup__hint { + font-size: 0.78rem; + color: #64748b; +} + +.sender-receiver-popup__loading { + font-size: 0.78rem; + color: #4f46e5; + font-weight: 600; +} + +.sender-receiver-popup__validation { + padding: 0.625rem 0.75rem; + border-radius: 10px; + background: rgba(239, 68, 68, 0.08); + color: #b91c1c; + font-size: 0.8rem; + font-weight: 600; +} + +.sender-receiver-popup__footer { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + width: 100%; +} + +.sender-receiver-popup__footer .dxbl-btn { + min-width: 148px; + border-radius: 8px; + font-weight: 600; +} + +.pdf-frame { + background: white; + border-radius: 16px; + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.25), + 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + position: relative; + flex: 1; + width: 95%; + display: flex; + flex-direction: row; + align-items: stretch; +} + +.pdf-frame::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%); + z-index: 1; + border-radius: 16px 16px 0 0; +} + +.pdf-canvas-wrapper { + flex: 1; + overflow: auto; + padding: 2rem; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; +} + +.pdf-page-container { + position: relative; + display: inline-block; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.pdf-canvas { + display: block; + vertical-align: top; + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + transition: opacity 0.15s ease-out; +} + +.pdf-canvas.rendering { + opacity: 0; +} + +.pdf-text-layer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + opacity: 1; + line-height: 1.0; + pointer-events: auto; +} + +.pdf-text-layer > span { + color: transparent; + position: absolute; + white-space: pre; + cursor: text; + transform-origin: 0% 0%; +} + +.pdf-text-layer ::selection { + background: rgba(126, 34, 206, 0.3); +} + +.pdf-text-layer ::-moz-selection { + background: rgba(126, 34, 206, 0.3); +} + +.pdf-signature-layer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: visible; + pointer-events: none; + z-index: 20; +} + +.pdf-signature-layer .signature-button { + pointer-events: auto; +} + +.signature-button { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.signature-button:focus { + outline: 2px solid #7e22ce; + outline-offset: 2px; +} + +.signature-button:active { + transform: scale(0.98); +} + +.error-container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 2rem; +} + +.alert { + border-radius: 12px; + border: none; + padding: 2rem; + max-width: 600px; +} + +.alert-danger { + background: linear-gradient(135deg, #fff1f2 0%, #ffe4e6 100%); + color: #be123c; + border-left: 4px solid #e11d48; +} + +.alert-warning { + background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); + color: #92400e; + border-left: 4px solid #f59e0b; +} + +.spinner-border { + border-width: 0.35rem; +} + +@media (max-width: 768px) { +.envelope-content { + padding: 0.75rem; +} + +.pdf-thumbnails { + width: 180px; + border-radius: 0 0 0 16px; +} + +.pdf-thumbnails__content { + padding: 0.75rem; + gap: 0.5rem; +} + +.pdf-toolbar { + flex-wrap: wrap; + padding: 0.625rem 1rem; + gap: 0.75rem; + width: 98%; + justify-content: center; +} + +.pdf-toolbar__divider { + display: none; +} + +.pdf-toolbar__zoom-section { + width: 100%; + max-width: 100%; +} + +.pdf-toolbar__zoom-slider { + min-width: 150px; +} + +.pdf-toolbar__btn--preset { + padding: 0.425rem 0.75rem; + font-size: 0.75rem; +} + +.pdf-frame { + border-radius: 12px; + width: 98%; + flex-direction: column; +} + +.pdf-canvas-wrapper { + padding: 1rem; +} + + .envelope-action-bar { + padding: 1rem 1.25rem; + } + + .envelope-action-bar__inner { + flex-wrap: wrap; + } + + .sender-receivers-panel { + padding: 0.625rem 0.75rem; + } + + .sender-receiver-chip { + width: 100%; + min-width: 0; + flex-wrap: wrap; + } + + .sender-receiver-chip__action { + width: 100%; + } + + .sender-receiver-chip__action { + width: 100%; + } + + .sender-receiver-popup__form-grid { + grid-template-columns: 1fr; + gap: 0.85rem; + min-height: 0; + } + + .envelope-title { + font-size: 1rem; + } + + .envelope-key { + font-size: 0.75rem; + } + + .envelope-logo svg { + width: 20px; + height: 20px; + } + + .alert { + padding: 1.5rem; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/FONT-LICENSE b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/FONT-LICENSE new file mode 100644 index 00000000..a1dc03f3 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/FONT-LICENSE @@ -0,0 +1,86 @@ +SIL OPEN FONT LICENSE Version 1.1 + +Copyright (c) 2014 Waybury + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/ICON-LICENSE b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/ICON-LICENSE new file mode 100644 index 00000000..2199f4a6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md new file mode 100644 index 00000000..5ac0c170 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/README.md @@ -0,0 +1,114 @@ +[Open Iconic v1.1.1](https://github.com/iconic/open-iconic) +=========== + +### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) + + + +## What's in Open Iconic? + +* 223 icons designed to be legible down to 8 pixels +* Super-light SVG files - 61.8 for the entire set +* SVG sprite—the modern replacement for icon fonts +* Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats +* Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats +* PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. + + +## Getting Started + +#### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. + +### General Usage + +#### Using Open Iconic's SVGs + +We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). + +``` +icon name +``` + +#### Using Open Iconic's SVG Sprite + +Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. + +Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* + +``` + + + +``` + +Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. + +``` +.icon { + width: 16px; + height: 16px; +} +``` + +Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. + +``` +.icon-account-login { + fill: #f00; +} +``` + +To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). + +#### Using Open Iconic's Icon Font... + + +##### …with Bootstrap + +You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` + + +``` + +``` + + +``` + +``` + +##### …with Foundation + +You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` + +``` + +``` + + +``` + +``` + +##### …on its own + +You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` + +``` + +``` + +``` + +``` + + +## License + +### Icons + +All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). + +### Fonts + +All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css new file mode 100644 index 00000000..4664f2e8 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css @@ -0,0 +1 @@ +@font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot new file mode 100644 index 00000000..f98177db Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf new file mode 100644 index 00000000..f6bd6846 Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg new file mode 100644 index 00000000..32b2c4e9 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf new file mode 100644 index 00000000..fab60486 Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff new file mode 100644 index 00000000..f9309988 Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/privacy-policy.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/privacy-policy.css new file mode 100644 index 00000000..12d2fc42 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/privacy-policy.css @@ -0,0 +1,41 @@ +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + line-height: 1.6; + margin: 1.25rem; + background-color: #f4f4f4; +} + +header { + text-align: center; + margin: 0rem 10rem 3rem 10rem; + box-shadow: 0 .25rem .5rem rgba(0, 0, 0, 0.1); +} + +h1 { + color: #333; +} + +h2 { + color: #0056b3; +} + +section { + background-color: white; + padding: 1.25rem; + border-radius: 0.5rem; + box-shadow: 0 .25rem .5rem rgba(0, 0, 0, 0.1); + margin: 0rem 10rem 3rem 10rem +} + +ul { + list-style: disc inside; +} + +a { + color: #0056b3; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/privacy-policy.min.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/privacy-policy.min.css new file mode 100644 index 00000000..eb1528de --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/privacy-policy.min.css @@ -0,0 +1 @@ +body{font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.6;margin:1.25rem;background-color:#f4f4f4}header{text-align:center;margin:0 10rem 3rem 10rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.1)}h1{color:#333}h2{color:#0056b3}section{background-color:#fff;padding:1.25rem;border-radius:.5rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.1);margin:0 10rem 3rem 10rem}ul{list-style:disc inside}a{color:#0056b3;text-decoration:none}a:hover{text-decoration:underline} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css new file mode 100644 index 00000000..9195af48 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/sender-page.css @@ -0,0 +1,297 @@ +.sender-dashboard-layout { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%); +} + +.sender-action-bar { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-bottom: 3px solid rgba(126, 34, 206, 0.3); + padding: 1rem 2rem; + flex-shrink: 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.sender-action-bar__inner { + max-width: 1600px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; +} + +.sender-title-section { + display: flex; + align-items: center; + gap: 1rem; +} + +.sender-logo svg { + filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3)); + color: #7e22ce; +} + +.sender-title { + font-size: 1.25rem; + font-weight: 700; + color: #1e293b; + letter-spacing: -0.025em; +} + +.sender-toolbar { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.sender-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.125rem; + background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%); + border: 1px solid rgba(126, 34, 206, 0.2); + border-radius: 8px; + font-size: 0.875rem; + font-weight: 600; + color: #1e293b; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + + .sender-btn:hover:not(:disabled) { + background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%); + border-color: rgba(126, 34, 206, 0.4); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2); + } + + .sender-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + background: rgba(0, 0, 0, 0.02); + border-color: rgba(0, 0, 0, 0.1); + } + +.sender-btn--primary { + background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%); + border-color: transparent; + color: white; +} + + .sender-btn--primary:hover:not(:disabled) { + background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%); + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(126, 34, 206, 0.3); + } + +.sender-btn--danger { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%); + border-color: rgba(239, 68, 68, 0.3); + color: #dc2626; +} + + .sender-btn--danger:hover:not(:disabled) { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + border-color: transparent; + color: white; + } + +.sender-btn--logout { + padding: 0.5rem; + min-width: 38px; +} + +.sender-content { + flex: 1; + min-height: 0; + padding: 1.5rem; + position: relative; + overflow: auto; +} + +.sender-grid-container { + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-radius: 16px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1); + overflow: hidden; + position: relative; + max-width: 1600px; + margin: 0 auto; +} + + .sender-grid-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%); + z-index: 1; + border-radius: 16px 16px 0 0; + } + +.sender-tabs { + display: flex; + border-bottom: 2px solid rgba(126, 34, 206, 0.1); + padding: 0 2rem; + background: rgba(126, 34, 206, 0.02); +} + +.sender-tab { + padding: 1rem 1.5rem; + font-size: 0.875rem; + font-weight: 600; + color: #6b7280; + background: transparent; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + + .sender-tab:hover { + color: #7e22ce; + background: rgba(126, 34, 206, 0.05); + } + +.sender-tab--active { + color: #7e22ce; + border-bottom-color: #7e22ce; + background: white; +} + +.sender-grid-wrapper { + padding: 1.5rem 2rem 2rem; +} + +/* Hide DevExpress empty cells */ +.dxbl-grid-empty-cell { + display: none !important; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.625rem; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.status-badge--partly-signed, +.status-badge--completed { + background: rgba(129, 199, 132, 0.15); + color: #2e7d32; +} + +.status-badge--queued, +.status-badge--sent { + background: rgba(255, 183, 77, 0.15); + color: #e65100; +} + +.status-badge--deleted, +.status-badge--rejected, +.status-badge--withdrawn { + background: rgba(229, 115, 115, 0.15); + color: #c62828; +} + +.status-badge--created, +.status-badge--saved { + background: rgba(100, 181, 246, 0.15); + color: #1565c0; +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; +} + +.status-dot--green { + background: #81c784; +} + +.status-dot--orange { + background: #ffb74d; +} + +.status-dot--red { + background: #e57373; +} + +.status-dot--blue { + background: #64b5f6; +} + +.receiver-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.125rem 0.5rem; + background: #f3f4f6; + border-radius: 4px; + font-size: 0.75rem; + color: #374151; + white-space: nowrap; +} + +.receiver-badge--signed { + background: rgba(129, 199, 132, 0.15); + color: #2e7d32; +} + +.receiver-badge--unsigned { + background: rgba(229, 115, 115, 0.15); + color: #c62828; +} + +@@media (max-width: 768px) { + .sender-action-bar { + padding: 1rem 1.25rem; + } + + .sender-action-bar__inner { + flex-wrap: wrap; + } + + .sender-toolbar { + width: 100%; + justify-content: flex-start; + } + + .sender-title { + font-size: 1.125rem; + } + + .sender-content { + padding: 0.75rem; + } + + .sender-grid-wrapper { + padding: 1rem; + } + + .sender-tabs { + padding: 0 1rem; + overflow-x: auto; + } + + .sender-tab { + padding: 0.875rem 1rem; + font-size: 0.813rem; + } +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/Document.pdf b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/Document.pdf new file mode 100644 index 00000000..e36adb6c Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/Document.pdf differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.de-DE.html b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.de-DE.html new file mode 100644 index 00000000..6b1bc8d4 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.de-DE.html @@ -0,0 +1,167 @@ + + + + + + + Datenschutzinformation für das Fernsignatursystem: signFLOW + + + + +
+

Datenschutzinformation für das Fernsignatursystem signFLOW

+

Stand: 18.11.2025

+
+ +
+

1. Allgemeine Informationen

+

In der heutigen schnelllebigen und zunehmend digitalen Welt, sind personenbezogene Daten eine wichtige + Ressource. Ihre Daten sind wertvoll und müssen daher mit der durch diverse Gesetze und Vorschriften (DSGVO, + TDDDG, ...) gebotenen Sorgfalt behandelt werden.

+

Als Anbieter von lokalen Lösungen (OnPremise) setzt der Hersteller des signFLOWs, die Digital Data GmbH, + einen klaren Schwerpunkt auf Datenschutz und Datensicherheit. Für Sie bedeutet dies, dass nur die nötigsten + Daten erhoben und gespeichert werden (Datensparsamkeit bzw. Datenminimierung). Außerdem kommen bei der + Verarbeitung aktuelle und als sicher geltende Technologien zum Einsatz.

+

Kontaktdaten des Herstellers:

+
+ Digital Data GmbH
+ Ludwig-Rinn-Straße 16
+ 35452 Heuchelheim
+ https://digitaldata.works
+ info-flow@digitaldata.works
+ Telefon: 0049 641 202360
+
+

Kontakt zum Datenschutzbeauftragten: privacy-flow@digitaldata.works

+
+ +
+

2. Verantwortliche Stelle der Datenverarbeitung

+

Ihre Daten werden vertrauensvoll verarbeitet von:

+
+ Digital Data GmbH
+ Ludwig-Rinn-Straße 16
+ 35452 Heuchelheim
+ https://digitaldata.works
+ info-flow@digitaldata.works
+ Telefon: 0049 641 202360
+
+

Kontakt zu unserer Datenschutzbeauftragten: privacy-flow@digitaldata.works

+
+ +
+

3. Datenerhebung

+

3.1 Die folgenden Kategorien personenbezogener Daten werden verarbeitet

+
    +
  • Namen: Benutzername, Vor- und Zunamen sowie Ihre digitale Unterschrift
  • +
  • Kontaktdaten: Telefonnummer, Mobilfunknummer und E-Mail-Adresse
  • +
  • Technische Daten: IP-Adresse, Zeitpunkt des Zugriffs oder Zugriffsversuchs
  • +
+ +

3.2 Ursprung der personenbezogenen Daten

+

Sie haben die unter 3.1 stehenden Daten vorab an Ihren Geschäftspartner (die verantwortliche Stelle) + übermittelt. Diese Übermittlung kann mündlich per Telefon, im persönlichen Kontakt, per E-Mail oder per + Kontaktformular geschehen sein.

+

Ihre digitale Signatur übermitteln Sie eigenständig, bei der Unterzeichnung eines Dokuments.

+ +

3.3 Aufbewahrungsfristen / Speicherungsdauer

+
    +
  • Die automatische E-Mail Korrespondenz wird für 6 Jahre aufbewahrt.
  • +
  • Unterzeichnete Verträge werden für die Dauer ihrer Laufzeit + 10 Jahre aufbewahrt.
  • +
  • Der technische Vorgang wird in der signFLOW Softwarelösung je nach Dokumentart bzw. Vertragsart + unbegrenzt aufbewahrt.
  • +
+

Ihre personenbezogenen Daten werden aber grundsätzlich anonymisiert, wenn:

+
    +
  • Der Vertrag ausgelaufen ist und die gesetzliche Aufbewahrungszeit vorbei ist.
  • +
  • Der Vertrag von Ihnen abgelehnt wurde bzw. nie unterschrieben wurde.
  • +
+

Die gesetzlichen Grundlagen dieser Aufbewahrungsfristen bieten unter anderen:

+
    +
  • Handelsgesetzbuch (HGB)
  • +
  • Abgabenordnung (AO)
  • +
  • Grundsätze zur ordnungsgemäßen Führung und Aufbewahrung von Büchern, Aufzeichnungen und Unterlagen in + elektronischer Form sowie zum Datenzugriff (GoBD)
  • +
+

+ Abhängig von der spezifischen Dokumentart, kann die Aufbewahrungsfrist variieren. Außerdem können sich + Zeiten + verlängern, falls Unregelmäßigkeiten auftreten. Z.B.: eine rechtliche Auseinandersetzung droht oder aktuell + läuft. +

+ +

3.4 Verarbeitungszweck

+

Die unter 3.1 definierten personenbezogenen Daten werden verarbeitet um:

+
    +
  • Den technisch notwendigen Prozess abzubilden bzw. anbieten zu können.
  • +
  • Damit Sie als Endbenutzer in der Lage sind, ein zu unterzeichnendes Dokument, digital zu unterschreiben, + ist die Feststellung der Identität des Antragstellers, Antragsprüfung und -abwicklung, Abrechnung sowie + die Wahrung der Dokumentationspflichten notwendig.
  • +
+

In Einzelfällen werden Daten zur Fehlerbehebung insbesondere bei Supportanfragen von der IT-Abteilung + gesondert behandelt oder ggf. an den Hersteller weitergegeben.

+

Im Zuge der Maßnahmen zur Sicherstellung der Informationssicherheit, insbesondere zur Identifizierung und + Abwehr von Angriffen, sowie zur Durchführung interner und externer Audits, der Exportkontrolle und der + Prüfung von Sanktionslisten, erfolgt ebenfalls eine Datenverarbeitung. Bei Anfragen gemäß §8 Abs. 2 VDG + werden die entsprechenden Informationen an die zuständigen Stellen übermittelt.

+ +

3.5 Rechtmäßigkeit der Verarbeitung

+

Ihre Daten werden auf Grundlage einer sich anbahnenden oder bereits vorhandenen Geschäftsbeziehung erhoben. +

+

Die rechtliche Grundlage für die Übermittlung an zuständige Stellen bildet §8 Abs. 2 VDG. Anfragen von + Betroffenen werden gemäß den Artikeln 12 bis 23 der DSGVO sowie den Paragraphen 32 bis 37 des BDSG + bearbeitet.

+ +

3.6 Berechtigte Interessen

+

Ein berechtigtes Interesse der verantwortlichen Stelle gemäß Art. 6 Abs. 1 lit. f DSGVO besteht in den + folgenden Fällen:

+

Es werden Maßnahmen zur Informationssicherheit ergriffen, die sowohl präventive technische als auch + organisatorische Maßnahmen sowie die Behandlung von Vorfällen umfassen. Ziel ist es, potenzielle Schäden für + das Unternehmen, die von der Datenverarbeitung betroffenen Personen und die Nutzer der Vertrauensdienste zu + bewerten und zu vermeiden.

+ +

3.7 Erforderlichkeit der Daten

+

Die erhobenen Daten stellen das Minimum der nötigen Daten zwecks digitaler Unterschrift dar. Ohne die unter + 3.1 genannten Daten, kann der Dienst nicht betrieben werden.

+

Besonders wichtig ist die Angabe einer Mobilfunknummer oder einer deutschen Festnetznummer, da diese für die + Authentifizierung und die Signaturauslösung als zweiter Faktor verwendet wird. Ohne diesen + Sicherheitsmechanismus kann der Dienst leider nicht bereitgestellt werden.

+ +

3.8 Weitergabe von Daten

+

Eine systemische Übermittlung von Daten findet nicht statt.

+

Daten werden nur in Ausnahmefällen, zwecks Supportleistungen, an den Hersteller weitergegeben. Mit der + Hersteller besteht ein gültiger Auftragsdatenverarbeitungsvertrag (AVV), welcher die Sicherheit und + Integrität des Umgangs mit Ihren Daten gewährleistet.

+
+ +
+

4. Verwendung von Cookies

+

+ Beim Besuch bestimmter Seiten kommen temporäre Cookies zum Einsatz, die für die technische Bereitstellung + der Dienste notwendig sind. Diese sogenannten Session-Cookies enthalten keine personenbezogenen Daten und + werden nach Beendigung der Sitzung automatisch gelöscht. Methoden wie Java-Applets oder Active-X-Controls, + die das Nutzerverhalten nachvollziehbar machen könnten, werden nicht verwendet. +

+
+ +
+

5. Betroffenenrechte

+

+ Wenn Sie Fragen zu Ihren Daten haben oder eine Berichtigung, Löschung oder Einschränkung der Verarbeitung + wünschen, senden Sie bitte Ihre Anfrage per Post oder E-Mail an die oben angegebene Adresse. Dies gilt auch, + wenn Sie gemäß Art. 21 DSGVO Widerspruch gegen die Verarbeitung einlegen möchten oder eine Anfrage zur + Datenübertragbarkeit haben. +

+

+ Bei Fragen oder Beschwerden zu einem Verfahren können Sie sich ebenfalls über die genannten + Kontaktmöglichkeiten an uns wenden. Sollten Sie darüber hinaus einen Grund zur Beschwerde haben, haben Sie + die Möglichkeit, sich an unsere Aufsichtsbehörde zu wenden. Welche Aufsichtsbehörde für Sie zuständig ist, + erfahren Sie hier: + Laender-node.html +

+
+ + + \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.en-US.html b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.en-US.html new file mode 100644 index 00000000..8117bfe5 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.en-US.html @@ -0,0 +1,158 @@ + + + + + + + Data Protection Information for the Remote Signature System signFLOW + + + + +
+

Data Protection Information for the Remote Signature System: signFLOW

+

As of: 18.11.2025

+
+
+

1. General Information

+

In today's fast-paced and increasingly digital world, personal data is an important resource. Your data is + valuable and must therefore be handled with the care required by various laws and regulations (GDPR, TDDDG, + ...).

+

As a provider of local solutions (OnPremise), the manufacturer of signFLOW, Digital Data GmbH, places a clear + focus on data protection and data security. For you, this means that only the necessary data is collected + and stored (data minimization). Furthermore, current and secure technologies are used in processing.

+

Contact details of the manufacturer:

+
+ Digital Data GmbH
+ Ludwig-Rinn-Straße 16
+ 35452 Heuchelheim
+ https://digitaldata.works
+ info-flow@digitaldata.works
+ Phone: 0049 641 202360
+
+

Contact the Data Protection Officer: privacy-flow@digitaldata.works

+
+ +
+

2. Responsible Entity for Data Processing

+

Your data is processed with confidence by:

+
+ Digital Data GmbH
+ Ludwig-Rinn-Straße 16
+ 35452 Heuchelheim
+ https://digitaldata.works
+ info-flow@digitaldata.works
+ Phone: 0049 641 202360
+
+

Contact our Data Protection Officer: privacy-flow@digitaldata.works

+
+ +
+

3. Data Collection

+

3.1 The following categories of personal data are processed

+
    +
  • Names: Username, first and last names as well as your digital signature
  • +
  • Contact details: Phone number, mobile phone number, and email address
  • +
  • Technical data: IP address, time of access, or access attempts
  • +
+ +

3.2 Source of the personal data

+

You have previously provided the data mentioned under 3.1 to your business partner (the responsible entity). + This transmission may have occurred verbally over the phone, in personal contact, via email, or via a + contact form.

+

You transmit your digital signature independently when signing a document.

+ +

3.3 Retention periods / Storage duration

+
    +
  • Automatic email correspondence is stored for 6 years.
  • +
  • Signed contracts are retained for the duration of their term + 10 years.
  • +
  • The technical process is stored in the signFLOW software solution indefinitely, depending on the + document or contract type.
  • +
+

Your personal data will generally be anonymized when:

+
    +
  • The contract has expired, and the statutory retention period is over.
  • +
  • The contract was rejected by you or never signed.
  • +
+

The legal basis for these retention periods includes:

+
    +
  • Commercial Code (HGB)
  • +
  • Tax Code (AO)
  • +
  • Principles for the Proper Keeping and Retention of Books, Records, and Documents in Electronic Form and + for Data Access (GoBD)
  • +
+

+ Depending on the specific type of document, the retention period may vary. Additionally, the periods may be + extended in case of irregularities, such as a pending or ongoing legal dispute. +

+ +

3.4 Purpose of processing

+

The personal data defined under 3.1 is processed to:

+
    +
  • Support or provide the technically necessary process.
  • +
  • Enable you, as the end user, to sign a document digitally. This requires the identification of the + applicant, application verification and processing, billing, and compliance with documentation + requirements.
  • +
+

In individual cases, data is processed separately by the IT department, particularly in response to support + requests, or possibly forwarded to the manufacturer for further processing.

+

Data processing also occurs to ensure information security, especially for the identification and prevention + of attacks, and for conducting internal and external audits, export controls, and sanctions list checks. + Information may also be transmitted to the relevant authorities in accordance with Section 8 (2) VDG.

+ +

3.5 Legality of processing

+

Your data is collected based on an impending or already existing business relationship.

+

The legal basis for the transmission to competent authorities is Section 8 (2) VDG. Requests from data + subjects are processed in accordance with Articles 12 to 23 of the GDPR and Sections 32 to 37 of the Federal + Data Protection Act (BDSG).

+ +

3.6 Legitimate interests

+

A legitimate interest of the responsible entity in accordance with Article 6 (1) (f) GDPR exists in the + following cases:

+

Measures are taken for information security, which include both preventive technical and organizational + measures as well as incident handling. The aim is to assess and avoid potential harm to the company, the + individuals affected by data processing, and the users of trust services.

+ +

3.7 Necessity of data

+

The collected data represents the minimum necessary for the digital signature. Without the data mentioned + under 3.1, the service cannot be operated.

+

It is particularly important to provide a mobile number or a German landline number, as this is used for + authentication and signature triggering as a second factor. Without this security mechanism, the service + cannot be provided.

+ +

3.8 Data transfer

+

Systematic data transmission does not take place.

+

Data is only forwarded to the manufacturer for support services in exceptional cases. A valid data processing + agreement (DPA) exists with the manufacturer, which ensures the security and integrity of the handling of + your data.

+
+ +
+

4. Use of Cookies

+

+ When visiting certain pages, temporary cookies are used, which are necessary for the technical provision of + the services. These so-called session cookies do not contain any personal data and are automatically deleted + after the session ends. Methods such as Java applets or Active-X controls that could track user behavior are + not used. +

+
+ +
+

5. Rights of Affected Persons

+

+ If you have questions about your data or wish to request correction, deletion, or restriction of processing, + please send your request by mail or email to the address provided above. This also applies if you wish to + object to the processing in accordance with Article 21 GDPR or request data portability. +

+

+ If you have questions or complaints about a procedure, you can also contact us using the contact details + provided. If you have further grounds for complaint, you can contact our supervisory authority. You can find + out which supervisory authority is responsible for you here: + Laender-node.html +

+
+ + + \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.fr-FR.html b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.fr-FR.html new file mode 100644 index 00000000..9299b72d --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/docs/privacy-policy.fr-FR.html @@ -0,0 +1,126 @@ + + + + + + + Informations sur la protection des données pour le système de signature à distance signFLOW + + + + +
+

Informations sur la protection des données pour le système de signature à distance : signFLOW

+

À jour au : 18.11.2025

+
+ +
+

1. Informations générales

+

Dans le monde actuel, rapide et de plus en plus numérique, les données personnelles sont une ressource précieuse. Vos données sont importantes et doivent donc être traitées avec le soin requis par les différentes lois et réglementations (RGPD, TDDDG, ...).

+

En tant que fournisseur de solutions locales (OnPremise), le fabricant de signFLOW, Digital Data GmbH, accorde une attention particulière à la protection et à la sécurité des données. Pour vous, cela signifie que seules les données nécessaires sont collectées et stockées (minimisation des données). De plus, des technologies actuelles et sécurisées sont utilisées lors du traitement.

+

Coordonnées du fabricant :

+
+ Digital Data GmbH
+ Ludwig-Rinn-Straße 16
+ 35452 Heuchelheim
+ https://digitaldata.works
+ info-flow@digitaldata.works
+ Téléphone : 0049 641 202360
+
+

Contactez le délégué à la protection des données : privacy-flow@digitaldata.works

+
+ +
+

2. Responsable du traitement des données

+

Vos données sont traitées en toute confiance par :

+
+ Digital Data GmbH
+ Ludwig-Rinn-Straße 16
+ 35452 Heuchelheim
+ https://digitaldata.works
+ info-flow@digitaldata.works
+ Téléphone : 0049 641 202360
+
+

Contactez notre délégué à la protection des données : privacy-flow@digitaldata.works

+
+ +
+

3. Collecte des données

+

3.1 Catégories de données personnelles traitées

+
    +
  • Noms : nom d’utilisateur, prénom et nom ainsi que votre signature numérique
  • +
  • Coordonnées : numéro de téléphone, numéro de mobile et adresse e-mail
  • +
  • Données techniques : adresse IP, heure d’accès ou tentatives d’accès
  • +
+ +

3.2 Source des données personnelles

+

Vous avez précédemment fourni les données mentionnées en 3.1 à votre partenaire commercial (le responsable du traitement). Cette transmission a pu se faire verbalement par téléphone, lors d’un contact personnel, par e-mail ou via un formulaire de contact.

+

Vous transmettez votre signature numérique de manière autonome lors de la signature d’un document.

+ +

3.3 Durée de conservation / stockage

+
    +
  • La correspondance par e-mail automatique est conservée pendant 6 ans.
  • +
  • Les contrats signés sont conservés pendant leur durée + 10 ans.
  • +
  • Le processus technique est stocké dans la solution logicielle signFLOW indéfiniment, selon le type de document ou de contrat.
  • +
+

Vos données personnelles seront généralement anonymisées lorsque :

+
    +
  • Le contrat a expiré et la période légale de conservation est terminée.
  • +
  • Le contrat a été refusé par vous ou jamais signé.
  • +
+

La base légale de ces périodes de conservation comprend :

+
    +
  • Code de commerce (HGB)
  • +
  • Code fiscal (AO)
  • +
  • Principes de tenue correcte et de conservation des livres, registres et documents sous forme électronique et d’accès aux données (GoBD)
  • +
+

+ Selon le type spécifique de document, la période de conservation peut varier. De plus, ces périodes peuvent être prolongées en cas d’irrégularités, telles qu’un litige en cours ou imminent. +

+ +

3.4 Finalité du traitement

+

Les données personnelles définies en 3.1 sont traitées pour :

+
    +
  • Supporter ou fournir le processus techniquement nécessaire.
  • +
  • Permettre à l’utilisateur final de signer un document numériquement. Cela nécessite l’identification du demandeur, la vérification et le traitement de la demande, la facturation et le respect des obligations documentaires.
  • +
+

Dans certains cas, les données sont traitées séparément par le département informatique, notamment en réponse à des demandes de support, ou éventuellement transmises au fabricant pour un traitement supplémentaire.

+

Le traitement des données intervient également pour garantir la sécurité de l’information, notamment pour identifier et prévenir les attaques, et pour effectuer des audits internes et externes, le contrôle des exportations et les vérifications des listes de sanctions. Les informations peuvent également être transmises aux autorités compétentes conformément à l’article 8 (2) VDG.

+ +

3.5 Licéité du traitement

+

Vos données sont collectées sur la base d’une relation commerciale existante ou imminente.

+

La base légale pour la transmission aux autorités compétentes est l’article 8 (2) VDG. Les demandes des personnes concernées sont traitées conformément aux articles 12 à 23 du RGPD et aux articles 32 à 37 de la loi fédérale sur la protection des données (BDSG).

+ +

3.6 Intérêts légitimes

+

Un intérêt légitime du responsable du traitement conformément à l’article 6 (1) (f) RGPD existe dans les cas suivants :

+

Des mesures sont prises pour la sécurité de l’information, incluant à la fois des mesures techniques et organisationnelles préventives ainsi que la gestion des incidents. L’objectif est d’évaluer et d’éviter les dommages potentiels pour l’entreprise, les personnes concernées par le traitement des données et les utilisateurs des services de confiance.

+ +

3.7 Nécessité des données

+

Les données collectées représentent le minimum nécessaire pour la signature numérique. Sans les données mentionnées en 3.1, le service ne peut pas être opéré.

+

Il est particulièrement important de fournir un numéro de mobile ou un numéro fixe allemand, car cela est utilisé pour l’authentification et le déclenchement de la signature comme second facteur. Sans ce mécanisme de sécurité, le service ne peut être fourni.

+ +

3.8 Transfert de données

+

Aucun transfert systématique de données n’a lieu.

+

Les données ne sont transmises au fabricant pour le support que dans des cas exceptionnels. Un accord de traitement des données (DPA) valide existe avec le fabricant, garantissant la sécurité et l’intégrité de la gestion de vos données.

+
+ +
+

4. Utilisation des cookies

+

+ Lors de la visite de certaines pages, des cookies temporaires sont utilisés, nécessaires à la fourniture technique des services. Ces cookies de session ne contiennent aucune donnée personnelle et sont automatiquement supprimés à la fin de la session. Aucune méthode telle que les applets Java ou les contrôles Active-X, pouvant suivre le comportement de l’utilisateur, n’est utilisée. +

+
+ +
+

5. Droits des personnes concernées

+

+ Si vous avez des questions concernant vos données ou souhaitez demander une correction, suppression ou limitation du traitement, veuillez envoyer votre demande par courrier ou e-mail à l’adresse indiquée ci-dessus. Cela s’applique également si vous souhaitez vous opposer au traitement conformément à l’article 21 RGPD ou demander la portabilité des données. +

+

+ Si vous avez des questions ou des plaintes concernant une procédure, vous pouvez également nous contacter via les coordonnées fournies. Si vous avez d’autres motifs de plainte, vous pouvez contacter notre autorité de contrôle. Vous pouvez vérifier quelle autorité de contrôle est compétente pour vous ici : + Laender-node.html +

+
+ + + \ No newline at end of file diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/fake-data/annotations.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/fake-data/annotations.json new file mode 100644 index 00000000..333c83b2 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/fake-data/annotations.json @@ -0,0 +1,20 @@ +[ + { + "id": 1, + "page": 1, + "x": 390.0, + "y": 380.0 + }, + { + "id": 2, + "page": 2, + "x": 390.0, + "y": 680.0 + }, + { + "id": 3, + "page": 3, + "x": 390.0, + "y": 980.0 + } +] diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/fonts/opensans.ttf b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/fonts/opensans.ttf new file mode 100644 index 00000000..ac587b48 Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/fonts/opensans.ttf differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/images/banner.png b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/images/banner.png new file mode 100644 index 00000000..2da0ee1d Binary files /dev/null and b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/images/banner.png differ diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/images/sad.svg b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/images/sad.svg new file mode 100644 index 00000000..aab2fdda --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/images/sad.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js new file mode 100644 index 00000000..377512f2 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/envelope-editor.js @@ -0,0 +1,95 @@ +window.envelopeEditor = { + + /** + * Returns the click position normalised to [0,1] relative to the rendered PDF page + * element inside DxPdfViewer (or DxReportViewer as fallback). + * + * Normalising means the result is independent of zoom level: no matter how much the + * user has zoomed in/out, the same physical spot on the PDF will always yield the same + * normalised value. C# multiplies by the page's point dimensions to get PDF points. + * + * @param {string} viewerCssClass - CssClass set on DxPdfViewer (e.g. "sender-editor-pdf-viewer") + * @param {number} clientX - MouseEvent.clientX from Blazor + * @param {number} clientY - MouseEvent.clientY from Blazor + * @returns {{ normX, normY, pageIndex } | null} + * normX / normY : 0..1 fraction within the page element + * pageIndex : 0-based index of the page the click landed on (-1 if not found) + */ + getClickCoordsOnPdfPage: function (viewerCssClass, clientX, clientY) { + + // Find the viewer root element + const viewer = document.querySelector('.' + viewerCssClass); + if (!viewer) { + console.warn('[envelopeEditor] viewer not found for class:', viewerCssClass); + return null; + } + + // --- Candidate page elements (ordered by preference) --- + // DxPdfViewer renders individual pages as .dxbl-pdfv-page elements. + // DxReportViewer uses .dxbrv-report-preview-content-img as fallback. + const pageSelectors = [ + '.dxbl-pdfv-page', + '.dxbrv-report-preview-page', + '.dxbrv-report-preview-content-img', + ]; + + let allPages = []; + for (const sel of pageSelectors) { + const found = Array.from(viewer.querySelectorAll(sel)); + if (found.length > 0) { + allPages = found; + break; + } + } + + if (allPages.length === 0) { + console.warn('[envelopeEditor] no page elements found inside viewer'); + return null; + } + + // --- Find which page the click landed on --- + // Walk through all pages; pick the one whose bounding rect contains the click. + // If none contains it exactly, fall back to the page closest vertically. + let targetPage = null; + let targetIndex = -1; + let minDist = Infinity; + + for (let i = 0; i < allPages.length; i++) { + const rect = allPages[i].getBoundingClientRect(); + + // Exact hit + if (clientX >= rect.left && clientX <= rect.right && + clientY >= rect.top && clientY <= rect.bottom) { + targetPage = allPages[i]; + targetIndex = i; + break; + } + + // Track closest page (vertical centre distance) as fallback + const cy = rect.top + rect.height / 2; + const dist = Math.abs(clientY - cy); + if (dist < minDist) { + minDist = dist; + targetPage = allPages[i]; + targetIndex = i; + } + } + + if (!targetPage) return null; + + const pageRect = targetPage.getBoundingClientRect(); + + // Clamp click inside page boundaries before normalising + const clampedX = Math.max(pageRect.left, Math.min(clientX, pageRect.right)); + const clampedY = Math.max(pageRect.top, Math.min(clientY, pageRect.bottom)); + + const normX = (clampedX - pageRect.left) / pageRect.width; + const normY = (clampedY - pageRect.top) / pageRect.height; + + return { + normX: normX, + normY: normY, + pageIndex: targetIndex + }; + } +}; diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js new file mode 100644 index 00000000..20998519 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js @@ -0,0 +1,1138 @@ +// PDF.js Viewer for Blazor WASM +window.pdfViewer = { + pdfDoc: null, + pageNum: 1, + pageRendering: false, + pageNumPending: null, + scale: 1.5, + canvas: null, + ctx: null, + totalPages: 0, + currentRenderTask: null, + dotNetReference: null, + wheelEventAttached: false, + _renderLock: false, // Lock to prevent concurrent renders + + // Quality options (configurable from appsettings.json) + qualityOptions: { + thumbnailBaseScale: 0.75, + thumbnailEnableHiDPI: true, + thumbnailMaxDPR: 2.0, + mainCanvasEnableHiDPI: true, + mainCanvasMaxDPR: 2.0, + enableSmoothZoom: true, + zoomTransitionDuration: 150, + renderingOpacity: 0.85, + zoomStepPercentage: 5 + }, + + setQualityOptions(options) { + this.qualityOptions = { ...this.qualityOptions, ...options }; + + // Apply CSS variables for dynamic styling + document.documentElement.style.setProperty('--zoom-transition-duration', `${options.zoomTransitionDuration}ms`); + document.documentElement.style.setProperty('--rendering-opacity', options.renderingOpacity); + }, + + async initialize(canvasId, pdfDataUrl, dotNetRef) { + try { + this.dotNetReference = dotNetRef; + + if (typeof window.pdfjsLib === 'undefined') { + await this.waitForPdfJs(); + } + + const pdfjsLib = window.pdfjsLib; + pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + + this.canvas = document.getElementById(canvasId); + if (!this.canvas) { + return false; + } + + this.ctx = this.canvas.getContext('2d'); + this.attachWheelEvent(); + + const uint8Array = this.base64ToUint8Array(pdfDataUrl); + const loadingTask = pdfjsLib.getDocument({ data: uint8Array }); + this.pdfDoc = await loadingTask.promise; + this.totalPages = this.pdfDoc.numPages; + + await this.renderPage(this.pageNum); + return true; + } catch (error) { + console.error('PDF viewer initialization failed:', error); + return false; + } + }, + + attachWheelEvent() { + if (this.wheelEventAttached) return; + + // Attach to the entire document body for global zoom control + document.body.addEventListener('wheel', (e) => { + // Check if Ctrl key is pressed (zoom gesture) + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + + const step = this.qualityOptions.zoomStepPercentage / 100; // Convert to decimal + + if (e.deltaY < 0) { + // Scroll up = Zoom In + this.scale = Math.min(this.scale + step, 3.0); + this.queueRenderPage(this.pageNum); + if (this.dotNetReference) { + this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale); + } + } else { + // Scroll down = Zoom Out + this.scale = Math.max(this.scale - step, 0.5); + this.queueRenderPage(this.pageNum); + if (this.dotNetReference) { + this.dotNetReference.invokeMethodAsync('OnZoomChanged', this.scale); + } + } + } + }, { passive: false }); + + this.wheelEventAttached = true; + }, + + async waitForPdfJs() { + for (let i = 0; i < 50; i++) { + if (typeof window.pdfjsLib !== 'undefined') { + return; + } + await new Promise(resolve => setTimeout(resolve, 100)); + } + throw new Error('PDF.js failed to load after 5 seconds'); + }, + + base64ToUint8Array(base64) { + // Remove data URL prefix if present + const base64String = base64.includes(',') ? base64.split(',')[1] : base64; + const raw = atob(base64String); + const uint8Array = new Uint8Array(raw.length); + for (let i = 0; i < raw.length; i++) { + uint8Array[i] = raw.charCodeAt(i); + } + return uint8Array; + }, + + async renderPage(num) { + // CRITICAL: Single render at a time - use a lock + if (this._renderLock) { + // Another render is in progress, queue it + this.pageNumPending = num; + return; + } + + this._renderLock = true; + + // Cancel any existing render task + if (this.currentRenderTask) { + try { + this.currentRenderTask.cancel(); + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (e) { + // Ignore cancellation errors + } + this.currentRenderTask = null; + } + + this.pageRendering = true; + + // Add rendering class for smooth transition (if enabled) + if (this.qualityOptions.enableSmoothZoom) { + this.canvas.classList.add('rendering'); + } + + try { + // Get scroll container + const container = this.canvas.closest('.pdf-canvas-wrapper'); + + // Store scroll position and viewport center BEFORE rendering + let scrollLeft = 0, scrollTop = 0; + let centerX = 0, centerY = 0; + let oldWidth = this.canvas.width; + let oldHeight = this.canvas.height; + + if (container) { + scrollLeft = container.scrollLeft; + scrollTop = container.scrollTop; + centerX = scrollLeft + container.clientWidth / 2; + centerY = scrollTop + container.clientHeight / 2; + } + + const page = await this.pdfDoc.getPage(num); + + // HiDPI support for main canvas (configurable) + const dpr = this.qualityOptions.mainCanvasEnableHiDPI + ? Math.min(window.devicePixelRatio || 1, this.qualityOptions.mainCanvasMaxDPR) + : 1.0; + const viewport = page.getViewport({ scale: this.scale * dpr }); + + // Set internal canvas resolution (high quality) + this.canvas.width = viewport.width; + this.canvas.height = viewport.height; + + // Set CSS display size (visual size) + this.canvas.style.width = `${viewport.width / dpr}px`; + this.canvas.style.height = `${viewport.height / dpr}px`; + + const renderContext = { + canvasContext: this.ctx, + viewport: viewport + }; + + // Enable high-quality rendering + this.ctx.imageSmoothingEnabled = true; + this.ctx.imageSmoothingQuality = 'high'; + + // Start new render task + this.currentRenderTask = page.render(renderContext); + await this.currentRenderTask.promise; + + // Render text layer for copy-paste functionality + await this.renderTextLayer(page, viewport, dpr); + + // Restore viewport center position AFTER rendering + if (container && oldWidth > 0 && oldHeight > 0) { + const scaleX = this.canvas.width / oldWidth; + const scaleY = this.canvas.height / oldHeight; + + const newCenterX = centerX * scaleX; + const newCenterY = centerY * scaleY; + + container.scrollLeft = newCenterX - container.clientWidth / 2; + container.scrollTop = newCenterY - container.clientHeight / 2; + } + + // Remove rendering class after completion + if (this.qualityOptions.enableSmoothZoom) { + this.canvas.classList.remove('rendering'); + } + + this.currentRenderTask = null; + this.pageRendering = false; + + } catch (error) { + if (error.name !== 'RenderingCancelledException') { + console.error('Render error:', error); + } + this.canvas.classList.remove('rendering'); + this.currentRenderTask = null; + this.pageRendering = false; + } finally { + // Always release lock + this._renderLock = false; + + // Process pending render + if (this.pageNumPending !== null) { + const pendingPage = this.pageNumPending; + this.pageNumPending = null; + this.renderPage(pendingPage); + } + } + }, + + async renderTextLayer(page, viewport, dpr) { + try { + const textLayerDiv = document.getElementById('pdf-text-layer'); + if (!textLayerDiv) { + return; + } + + // Clear previous text layer + textLayerDiv.innerHTML = ''; + + // Set text layer dimensions to match canvas display size + textLayerDiv.style.width = `${viewport.width / dpr}px`; + textLayerDiv.style.height = `${viewport.height / dpr}px`; + + // Set --scale-factor CSS variable required by PDF.js + textLayerDiv.style.setProperty('--scale-factor', this.scale); + + // Get text content from PDF + const textContent = await page.getTextContent(); + + // Create viewport for text layer (without DPR scaling for positioning) + const textViewport = page.getViewport({ scale: this.scale }); + + // Render text layer using PDF.js built-in function + const pdfjsLib = window.pdfjsLib; + await pdfjsLib.renderTextLayer({ + textContentSource: textContent, + container: textLayerDiv, + viewport: textViewport, + textDivs: [] + }).promise; + + } catch (error) { + // Silently handle text layer errors (non-critical feature) + } + }, + + queueRenderPage(num) { + // Always use pending mechanism to avoid race conditions + if (this.pageRendering || this.currentRenderTask) { + this.pageNumPending = num; + } else { + this.renderPage(num); + } + }, + + nextPage() { + if (this.pageNum >= this.totalPages) { + return false; + } + this.pageNum++; + this.queueRenderPage(this.pageNum); + return true; + }, + + previousPage() { + if (this.pageNum <= 1) { + return false; + } + this.pageNum--; + this.queueRenderPage(this.pageNum); + return true; + }, + + goToPage(num) { + if (num < 1 || num > this.totalPages) { + return false; + } + this.pageNum = num; + this.queueRenderPage(this.pageNum); + return true; + }, + + async zoomIn() { + const step = this.qualityOptions.zoomStepPercentage / 100; + this.scale = Math.min(this.scale + step, 3.0); + await this.renderPage(this.pageNum); + }, + + async zoomOut() { + const step = this.qualityOptions.zoomStepPercentage / 100; + this.scale = Math.max(this.scale - step, 0.5); + await this.renderPage(this.pageNum); + }, + + setScale(scale) { + if (scale >= 0.5 && scale <= 3.0) { + this.scale = scale; + this.queueRenderPage(this.pageNum); + } + }, + + async fitToWidth() { + const container = this.canvas.closest('.pdf-frame'); + if (!container || !this.pdfDoc) return; + + try { + const page = await this.pdfDoc.getPage(this.pageNum); + const viewport = page.getViewport({ scale: 1.0 }); + + const containerWidth = container.clientWidth - 80; + const optimalScale = containerWidth / viewport.width; + + this.scale = Math.min(Math.max(optimalScale, 0.5), 3.0); + this.queueRenderPage(this.pageNum); + } catch (error) { + console.error('Error fitting to width:', error); + } + }, + + async renderThumbnail(pageNum, canvasId) { + if (!this.pdfDoc) { + return; + } + + try { + // Wait for canvas to be in DOM + let canvas = document.getElementById(canvasId); + let retries = 0; + while (!canvas && retries < 10) { + await new Promise(resolve => setTimeout(resolve, 100)); + canvas = document.getElementById(canvasId); + retries++; + } + + if (!canvas) { + return; + } + + // Check if canvas is already being used by a render task + if (canvas._renderTask) { + try { + await canvas._renderTask; + } catch (e) { + // Ignore cancellation errors + } + } + + const page = await this.pdfDoc.getPage(pageNum); + + // High-quality rendering with HiDPI support (configurable) + const dpr = this.qualityOptions.thumbnailEnableHiDPI + ? Math.min(window.devicePixelRatio || 1, this.qualityOptions.thumbnailMaxDPR) + : 1.0; + const baseScale = this.qualityOptions.thumbnailBaseScale; + const scale = baseScale * dpr; + + const viewport = page.getViewport({ scale: scale }); + + // Set actual canvas pixel dimensions (internal resolution) + canvas.width = viewport.width; + canvas.height = viewport.height; + + // Remove any inline styles - let CSS handle display size + canvas.style.width = ''; + canvas.style.height = ''; + + const ctx = canvas.getContext('2d'); + + // Enable maximum quality rendering + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + + const renderContext = { + canvasContext: ctx, + viewport: viewport + }; + + // Store render task on canvas to track active renders + const renderTask = page.render(renderContext); + canvas._renderTask = renderTask.promise; + + await renderTask.promise; + + // Clear the task when done + delete canvas._renderTask; + } catch (error) { + // Silently handle thumbnail errors (non-critical) + } + }, + + getCurrentPage() { + return this.pageNum; + }, + + getTotalPages() { + return this.totalPages; + }, + + getScale() { + return this.scale; + }, + + dispose() { + if (this.wheelEventAttached) { + this.wheelEventAttached = false; + this.dotNetReference = null; + } + this.detachResizeListeners(); + }, + + // Resizable splitter functionality + isResizing: false, + resizeMouseMoveHandler: null, + resizeMouseUpHandler: null, + + attachResizeListeners(dotNetRef) { + this.dotNetReference = dotNetRef; + + this.resizeMouseMoveHandler = (e) => { + if (this.isResizing && this.dotNetReference) { + this.dotNetReference.invokeMethodAsync('OnSplitterMouseMove', e.clientX); + } + }; + + this.resizeMouseUpHandler = () => { + if (this.isResizing && this.dotNetReference) { + this.isResizing = false; + this.dotNetReference.invokeMethodAsync('OnSplitterMouseUp'); + } + }; + + document.addEventListener('mousemove', this.resizeMouseMoveHandler); + document.addEventListener('mouseup', this.resizeMouseUpHandler); + }, + + detachResizeListeners() { + if (this.resizeMouseMoveHandler) { + document.removeEventListener('mousemove', this.resizeMouseMoveHandler); + this.resizeMouseMoveHandler = null; + } + if (this.resizeMouseUpHandler) { + document.removeEventListener('mouseup', this.resizeMouseUpHandler); + this.resizeMouseUpHandler = null; + } + this.isResizing = false; + }, + + startResize() { + this.isResizing = true; + }, + + // Signature button functionality + signatureButtons: [], + appliedSignatures: [], // Track which signatures have been applied (ID list) + appliedSignatureElements: [], // ✅ NEW: Track applied signature DOM elements + _lastViewedSignatureId: null, // Track last viewed signature for navigation + + /** + * Gets signature navigation state (for toolbar display) + * @returns {object} { total, signed, unsigned, currentIndex, canGoPrev, canGoNext } + */ + getSignatureNavState() { + // Return empty state if global signature list is empty + if (!this._allSignatures || this._allSignatures.length === 0) { + return { + total: 0, + signed: 0, + unsigned: 0, + currentIndex: -1, + canGoPrev: false, + canGoNext: false + }; + } + + + // Count signatures across ALL pages (from database global list) + const total = this._allSignatures.length; // Global: Total signature count + const signed = this.appliedSignatures.length; // Signed signatures + const unsigned = total - signed; // Calculated: Unsigned signatures + + // Find the current viewed signature index + let currentIndex = 0; + if (this._lastViewedSignatureId) { + const index = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId); + currentIndex = index !== -1 ? index + 1 : 0; // 1-based index (for user display) + } + + return { + total: total, + signed: signed, + unsigned: unsigned, + currentIndex: currentIndex, // Current signature index (1-5 range) + canGoPrev: total > 0, // Always active (if signatures exist) + canGoNext: total > 0 // Always active (if signatures exist) + }; + }, + + /** + * Navigates to the next unsigned signature button. + * Scrolls to button position and changes page if necessary. + * Cross-page navigation: searches ALL pages for next unsigned signature. + */ + async goToNextSignature(dotNetRef) { + // Exit if no global signature list + if (!this._allSignatures || this._allSignatures.length === 0) { + return false; + } + + // Find current displayed signature's index + let currentIndex = -1; + if (this._lastViewedSignatureId) { + currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId); + } + + // Get next signature (regardless of signed status) + let nextIndex = currentIndex + 1; + + // Infinite loop: If at last signature, return to first + if (nextIndex >= this._allSignatures.length) { + nextIndex = 0; // Return to first signature + } + + const nextSignature = this._allSignatures[nextIndex]; + + // Change page if signature is on different page + if (nextSignature.page !== this.pageNum) { + // Change page + this.pageNum = nextSignature.page; + this.queueRenderPage(this.pageNum); + + // Wait until render completes + let waitCount = 0; + while (this.pageRendering && waitCount < 20) { + await new Promise(resolve => setTimeout(resolve, 100)); + waitCount++; + } + + // Notify Blazor - re-render signature buttons + if (dotNetRef) { + await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum); + } + + // Wait for buttons to be added to DOM + await new Promise(resolve => setTimeout(resolve, 150)); + } + + + // Save last viewed signature + this._lastViewedSignatureId = nextSignature.id; + + // Check if signature is signed + const isApplied = this.appliedSignatures.some(s => s.id === nextSignature.id); + + if (isApplied) { + // Signed - find overlay container and scroll + const container = document.querySelector(`.applied-signature[data-signature-id="${nextSignature.id}"]`); + if (container) { + this.scrollToElement(container); + } + } else { + // Unsigned - find button and scroll + const button = this.signatureButtons.find(btn => + parseInt(btn.getAttribute('data-signature-id')) === nextSignature.id + ); + if (button) { + this.scrollToButton(button); + } + } + + // Update counter (notify Blazor) + if (dotNetRef) { + dotNetRef.invokeMethodAsync('OnSignatureNavChanged'); + } + + return true; + }, + + /** + * Navigates to the previous signature (last applied one). + * Scrolls to signature position and changes page if necessary. + */ + async goToPreviousSignature(dotNetRef) { + if (!this._allSignatures || this._allSignatures.length === 0) { + return false; + } + + // Find current displayed signature's index + let currentIndex = this._allSignatures.length; // Default: after last signature + if (this._lastViewedSignatureId) { + currentIndex = this._allSignatures.findIndex(s => s.id === this._lastViewedSignatureId); + } + + // Get previous signature + let prevIndex = currentIndex - 1; + + // Infinite loop: If at first signature, go to last + if (prevIndex < 0) { + prevIndex = this._allSignatures.length - 1; // Go to last signature + } + + const prevSignature = this._allSignatures[prevIndex]; + + // Change page if needed + if (prevSignature.page !== this.pageNum) { + // Change page + this.pageNum = prevSignature.page; + this.queueRenderPage(this.pageNum); + + // Wait until render completes + let waitCount = 0; + while (this.pageRendering && waitCount < 20) { + await new Promise(resolve => setTimeout(resolve, 100)); + waitCount++; + } + + // Notify Blazor - re-render signature buttons + if (dotNetRef) { + await dotNetRef.invokeMethodAsync('OnPageChangedBySignatureNav', this.pageNum); + } + + // Wait for DOM update + await new Promise(resolve => setTimeout(resolve, 150)); + } + + // Save last viewed signature + this._lastViewedSignatureId = prevSignature.id; + + // Check if signature is signed + const isApplied = this.appliedSignatures.some(s => s.id === prevSignature.id); + + if (isApplied) { + // Signed - find overlay container and scroll + const container = document.querySelector(`.applied-signature[data-signature-id="${prevSignature.id}"]`); + if (container) { + this.scrollToElement(container); + } + } else { + // Unsigned - find button and scroll + const button = this.signatureButtons.find(btn => + parseInt(btn.getAttribute('data-signature-id')) === prevSignature.id + ); + if (button) { + this.scrollToButton(button); + } + } + + // Notify Blazor + if (dotNetRef) { + dotNetRef.invokeMethodAsync('OnSignatureNavChanged'); + } + + return true; + }, + + /** + * Scrolls to center a button in the viewport + */ + scrollToButton(button) { + const wrapper = this.canvas.closest('.pdf-canvas-wrapper'); + if (!wrapper) return; + + const buttonRect = button.getBoundingClientRect(); + const wrapperRect = wrapper.getBoundingClientRect(); + + // Calculate scroll to center button + const scrollLeft = wrapper.scrollLeft + buttonRect.left - wrapperRect.left - (wrapperRect.width / 2) + (buttonRect.width / 2); + const scrollTop = wrapper.scrollTop + buttonRect.top - wrapperRect.top - (wrapperRect.height / 2) + (buttonRect.height / 2); + + wrapper.scrollTo({ + left: scrollLeft, + top: scrollTop, + behavior: 'smooth' + }); + }, + + /** + * Scrolls to center an element in the viewport + */ + scrollToElement(element) { + const wrapper = this.canvas.closest('.pdf-canvas-wrapper'); + if (!wrapper) return; + + const elemRect = element.getBoundingClientRect(); + const wrapperRect = wrapper.getBoundingClientRect(); + + const scrollLeft = wrapper.scrollLeft + elemRect.left - wrapperRect.left - (wrapperRect.width / 2) + (elemRect.width / 2); + const scrollTop = wrapper.scrollTop + elemRect.top - wrapperRect.top - (wrapperRect.height / 2) + (elemRect.height / 2); + + wrapper.scrollTo({ + left: scrollLeft, + top: scrollTop, + behavior: 'smooth' + }); + }, + + /** + * Renders clickable signature buttons on the PDF canvas. + * @param {Array} signatures - Array of SignatureDto objects with x, y coordinates in PDF POINTS + * @param {number} currentPageNum - Current page number (1-based) + * @param {object} dotNetRef - .NET reference for callbacks + */ + async renderSignatureButtons(signatures, currentPageNum, dotNetRef) { + // Clear existing buttons (NOT applied signatures!) + this.clearSignatureButtons(); + + if (!this.pdfDoc || !signatures || signatures.length === 0) { + return; + } + + this.dotNetReference = dotNetRef; + this._allSignatures = signatures; // Store for navigation + + try { + // CRITICAL: Filter OUT already applied signatures! + const appliedIds = new Set(this.appliedSignatures.map(s => s.id)); + const pageSignatures = signatures.filter(sig => + sig.page === currentPageNum && !appliedIds.has(sig.id) // ? Skip applied ones! + ); + + if (pageSignatures.length === 0) { + return; + } + + // Get current page and viewport + const page = await this.pdfDoc.getPage(currentPageNum); + const dpr = this.qualityOptions.mainCanvasEnableHiDPI + ? Math.min(window.devicePixelRatio || 1, this.qualityOptions.mainCanvasMaxDPR) + : 1.0; + const viewport = page.getViewport({ scale: this.scale * dpr }); + + // Get signature layer container + const signatureLayer = document.getElementById('pdf-signature-layer'); + if (!signatureLayer) { + console.warn('Signature layer not found'); + return; + } + + // Set signature layer dimensions to match canvas display size + signatureLayer.style.width = `${viewport.width / dpr}px`; + signatureLayer.style.height = `${viewport.height / dpr}px`; + + // Update applied signature coordinates for current zoom level + this.updateAppliedSignaturePositions(signatureLayer, currentPageNum); + + // Create button for each UNSIGNED signature + pageSignatures.forEach(sig => { + // Coordinates are in PDF POINTS - convert to display pixels + const xPx = (sig.x * this.scale); + const yPx = (sig.y * this.scale); + + // ✅ FIXED: Scale button size proportionally with zoom + const baseScale = 1.5; // Reference scale (initial load) + const scaleFactor = this.scale / baseScale; + const baseWidth = 150; + const baseHeight = 60; + const baseFontSize = 18; + const baseIconSize = 24; + + const scaledWidth = baseWidth * scaleFactor; + const scaledHeight = baseHeight * scaleFactor; + const scaledFontSize = Math.max(baseFontSize * scaleFactor, 10); // Min 10px + const scaledIconSize = baseIconSize * scaleFactor; + + // Create button element + const button = document.createElement('button'); + button.className = 'signature-button'; + button.setAttribute('data-signature-id', sig.id); + button.setAttribute('type', 'button'); + button.setAttribute('tabindex', '0'); + button.style.position = 'absolute'; + button.style.left = `${xPx}px`; + button.style.top = `${yPx}px`; + button.style.width = `${scaledWidth}px`; // ✅ Scaled + button.style.height = `${scaledHeight}px`; // ✅ Scaled + button.style.backgroundColor = '#4F46E5'; + button.style.color = 'white'; + button.style.border = 'none'; + button.style.borderRadius = '8px'; + button.style.cursor = 'pointer'; + button.style.fontSize = '16px'; + button.style.fontWeight = '600'; + button.style.display = 'flex'; + button.style.flexDirection = 'column'; + button.style.alignItems = 'center'; + button.style.justifyContent = 'center'; + button.style.gap = '4px'; + button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; + button.style.transition = 'all 0.2s ease'; + button.style.zIndex = '100'; + + // Add text + const textDiv = document.createElement('div'); + textDiv.textContent = 'Unterschreiben'; + textDiv.style.fontSize = `${scaledFontSize}px`; // ✅ Scaled + textDiv.style.fontWeight = '700'; + + // Add SVG icon + const svgNS = 'http://www.w3.org/2000/svg'; + const svg = document.createElementNS(svgNS, 'svg'); + svg.setAttribute('width', scaledIconSize); // ✅ Scaled + svg.setAttribute('height', scaledIconSize); // ✅ Scaled + svg.setAttribute('viewBox', '0 8 32 36'); + svg.setAttribute('fill', 'none'); + svg.style.filter = 'drop-shadow(0 1px 2px rgba(0,0,0,0.2))'; + + const path = document.createElementNS(svgNS, 'path'); + path.setAttribute('fill-rule', 'evenodd'); + path.setAttribute('clip-rule', 'evenodd'); + path.setAttribute('d', 'M25.061 6.90625L23.7115 8.25503C23.2861 8.05188 22.8241 7.9503 22.3621 7.9503C21.5605 7.9503 20.7589 8.25613 20.1483 8.86778L8.18147 20.8336L6.70565 26.7379H6.70557V27.7817H26.5372V26.7379H6.70671L12.6102 25.2623L24.576 13.2955C25.5404 12.3318 25.7445 10.8952 25.1882 9.73146L26.5369 8.38214L25.061 6.90625ZM23.174 10.27C22.9569 10.0539 22.6688 9.93388 22.362 9.93388C22.0551 9.93388 21.767 10.0539 21.5499 10.27L13.5323 18.2876L15.1564 19.9117L23.174 11.8941C23.6218 11.4463 23.6218 10.7177 23.174 10.27ZM14.4922 20.5759L12.868 18.9518L9.97241 21.8475L9.43069 24.0133L11.5965 23.4716L14.4922 20.5759Z'); + path.setAttribute('fill', 'white'); + + svg.appendChild(path); + button.appendChild(textDiv); + button.appendChild(svg); + + // Add hover effect + button.addEventListener('mouseenter', () => { + button.style.backgroundColor = '#4338CA'; + button.style.transform = 'scale(1.05)'; + button.style.boxShadow = '0 4px 12px rgba(79, 70, 229, 0.4)'; + }); + button.addEventListener('mouseleave', () => { + button.style.backgroundColor = '#4F46E5'; + button.style.transform = 'scale(1)'; + button.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; + }); + + // Add click handler + button.addEventListener('click', () => { + if (this.dotNetReference) { + this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id); + } + }); + + signatureLayer.appendChild(button); + this.signatureButtons.push(button); + }); + + } catch (error) { + console.error('Error rendering signature buttons:', error); + } + }, + + /** + * Scales an applied signature container based on current zoom level + * @param {HTMLElement} container - Applied signature container + * @param {number} currentScale - Current PDF zoom scale + */ + scaleAppliedSignature(container, currentScale) { + const baseScale = 1.5; // Reference scale (initial load) + const scaleFactor = currentScale / baseScale; + + // Scale width + const baseWidth = parseInt(container.getAttribute('data-base-width') || 230); + container.style.width = `${baseWidth * scaleFactor}px`; + + // Scale padding + const basePadding = parseInt(container.getAttribute('data-base-padding') || 12); + container.style.padding = `${basePadding * scaleFactor}px`; + + // Scale border radius (subtle detail) + const baseBorderRadius = parseInt(container.getAttribute('data-base-border-radius') || 6); + container.style.borderRadius = `${baseBorderRadius * scaleFactor}px`; + + // Scale font size (with min 6px for readability) + const infoContainer = container.querySelector('.signature-info-text'); + if (infoContainer) { + const baseFontSize = parseInt(infoContainer.getAttribute('data-base-font-size') || 9); + const scaledFontSize = Math.max(baseFontSize * scaleFactor, 6); + infoContainer.style.fontSize = `${scaledFontSize}px`; + } + + // Scale image max height + const baseImgHeight = parseInt(container.getAttribute('data-base-img-height') || 70); + const img = container.querySelector('img'); + if (img) { + img.style.maxHeight = `${baseImgHeight * scaleFactor}px`; + } + + // Scale separator line margin + const baseSeparatorMargin = 6; + const separators = container.querySelectorAll('div[style*="border-top"]'); + separators.forEach(sep => { + sep.style.marginTop = `${baseSeparatorMargin * scaleFactor}px`; + sep.style.marginBottom = `${(baseSeparatorMargin + 2) * scaleFactor}px`; + }); + }, + + /** + * Updates applied signature positions based on current zoom level + * @param {HTMLElement} signatureLayer - Signature layer container + * @param {number} currentPageNum - Current page number + */ + updateAppliedSignaturePositions(signatureLayer, currentPageNum) { + if (!signatureLayer || !this._allSignatures) return; + + const appliedContainers = signatureLayer.querySelectorAll('.applied-signature'); + appliedContainers.forEach(container => { + const signatureId = parseInt(container.getAttribute('data-signature-id')); + const signature = this._allSignatures.find(s => s.id === signatureId); + + if (signature) { + // ✅ Position calculation (same as renderSignatureButtons) + const xPx = signature.x * this.scale; + const yPx = signature.y * this.scale; + + container.style.left = `${xPx}px`; + container.style.top = `${yPx}px`; + + // ✅ FIXED: Apply comprehensive scaling using helper method + this.scaleAppliedSignature(container, this.scale); + + // Show/hide based on current page + container.style.display = (signature.page === currentPageNum) ? '' : 'none'; + } + }); + }, + + /** + * Clears all signature buttons from the canvas. + * Also hides applied signatures that don't belong to current page. + */ + clearSignatureButtons() { + // Remove unsigned signature buttons + this.signatureButtons.forEach(button => { + if (button.parentNode) { + button.parentNode.removeChild(button); + } + }); + this.signatureButtons = []; + + // ✅ FIXED: Update applied signatures (position + scaling) + this.appliedSignatureElements.forEach(container => { + const signatureId = parseInt(container.getAttribute('data-signature-id')); + const signature = this._allSignatures?.find(s => s.id === signatureId); + + if (signature) { + // Update position + const xPx = signature.x * this.scale; + const yPx = signature.y * this.scale; + container.style.left = `${xPx}px`; + container.style.top = `${yPx}px`; + + // Update scaling + this.scaleAppliedSignature(container, this.scale); + + // Show/hide based on current page + container.style.display = (signature.page === this.pageNum) ? '' : 'none'; + } + }); + }, + + /** + * Applies a signature to a specific signature field, removing the button and rendering the signature. + * German standard: Signature image + Name, Position, Place, Date + * @param {number} signatureId - ID of the signature field + * @param {string} signatureDataUrl - Base64 PNG data URL of signature + * @param {string} fullName - Signer's full name + * @param {string} position - Signer's position (optional, can be empty) + * @param {string} place - Signing place + */ + async applySignature(signatureId, signatureDataUrl, fullName, position, place) { + try { + // Find and remove the button + const buttonIndex = this.signatureButtons.findIndex(btn => { + return btn.getAttribute('data-signature-id') == signatureId; + }); + + if (buttonIndex === -1) { + console.warn(`Signature button #${signatureId} not found`); + return; + } + + const button = this.signatureButtons[buttonIndex]; + const signatureLayer = document.getElementById('pdf-signature-layer'); + + if (!signatureLayer) { + console.error('Signature layer not found'); + return; + } + + // Get button position before removing it + const left = button.style.left; + const top = button.style.top; + + // Find signature data for tracking + const signature = this._allSignatures?.find(s => s.id === signatureId); + + // Remove button + if (button.parentNode) { + button.parentNode.removeChild(button); + } + this.signatureButtons.splice(buttonIndex, 1); + + // Track applied signature + if (signature) { + this.appliedSignatures.push({ + id: signatureId, + page: signature.page + }); + } + + // Create signature container + const signatureContainer = document.createElement('div'); + signatureContainer.className = 'applied-signature'; + signatureContainer.setAttribute('data-signature-id', signatureId); + + // ✅ FIXED: Store base values for scaling + signatureContainer.setAttribute('data-base-width', '230'); + signatureContainer.setAttribute('data-base-padding', '12'); + signatureContainer.setAttribute('data-base-font-size', '9'); + signatureContainer.setAttribute('data-base-img-height', '70'); + signatureContainer.setAttribute('data-base-border-radius', '6'); + + signatureContainer.style.position = 'absolute'; + signatureContainer.style.left = left; + signatureContainer.style.top = top; + signatureContainer.style.width = '230px'; + signatureContainer.style.backgroundColor = '#f8f9fa'; + signatureContainer.style.border = '1px solid #dee2e6'; + signatureContainer.style.borderRadius = '6px'; + signatureContainer.style.padding = '12px'; + signatureContainer.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; + signatureContainer.style.fontFamily = "'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"; + + // Signature image + const signatureImg = document.createElement('img'); + signatureImg.src = signatureDataUrl; + signatureImg.alt = 'Unterschrift'; + signatureImg.style.width = '100%'; + signatureImg.style.height = 'auto'; + signatureImg.style.maxHeight = '70px'; + signatureImg.style.display = 'block'; + signatureImg.style.objectFit = 'contain'; + signatureImg.style.marginBottom = '6px'; + + // Separator line (German standard) + const separator = document.createElement('div'); + separator.style.width = '100%'; + separator.style.height = '1px'; + separator.style.backgroundColor = '#495057'; + separator.style.marginBottom = '8px'; + + // Text information container + const infoContainer = document.createElement('div'); + infoContainer.className = 'signature-info-text'; // ✅ Add class (for querySelector) + infoContainer.setAttribute('data-base-font-size', '9'); // ✅ Store base font size + infoContainer.style.fontSize = '9px'; + infoContainer.style.lineHeight = '1.4'; + infoContainer.style.color = '#495057'; + infoContainer.style.fontWeight = '400'; + + // Format date (German style: dd.MM.yyyy) + const today = new Date(); + const dateStr = today.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + + // Build text lines (German standard format) + const lines = []; + lines.push(`${this.escapeHtml(fullName)}`); + + if (position && position.trim() !== '') { + lines.push(`${this.escapeHtml(position)}`); + } + + lines.push(`${this.escapeHtml(place)}, ${dateStr}`); + + infoContainer.innerHTML = lines.join('
'); + + // Assemble container + signatureContainer.appendChild(signatureImg); + signatureContainer.appendChild(separator); + signatureContainer.appendChild(infoContainer); + + // Add to signature layer + signatureLayer.appendChild(signatureContainer); + + // ✅ FIXED: Track applied signature element for zoom updates + this.appliedSignatureElements.push(signatureContainer); + + // ✅ FIXED: Apply initial scaling based on current zoom + this.scaleAppliedSignature(signatureContainer, this.scale); + + console.log(`Signature #${signatureId} applied successfully`); + + } catch (error) { + console.error('Error applying signature:', error); + } + }, + + /** + * Escapes HTML to prevent XSS attacks + */ + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +}; + + + + + + + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js new file mode 100644 index 00000000..11bc80fc --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js @@ -0,0 +1,353 @@ +window.receiverSignature = (() => { + + // ?? State ??????????????????????????????????????????????????????????????? + const pads = new Map(); + const typedSignatures = new Map(); + const imageSignatures = new Map(); + const overlayButtons = new Map(); // annotationId -> { btn, signed } + let _dotNetRef = null; + + // DevExpress Blazor Report Viewer selectors (confirmed via debugDumpViewerDom) + const PAGE_IMG_SEL = '.dxbrv-report-preview-content-img'; + const SCROLL_CONTAINER_SEL = '.dxbrv-surface-wrapper'; + const VIEWER_WRAPPER_SEL = '.receiver-viewer-wrapper'; + + // DX report coordinate space (1/100 inch, A4) + const DX_PAGE_WIDTH = 827.0; + const DX_PAGE_HEIGHT = 1169.0; + + // Signature field size in DX units + const SIG_WIDTH_DX = 230.0; + const SIG_HEIGHT_DX = 154.0; + + // ?? Annotation Checkboxes ???????????????????????????????????????????????? + + // Active install context holds everything needed to reposition on resize/scroll + let _installCtx = null; + + function installAnnotationCheckboxes(annotations, checkedIds, dotNetRef) { + _dotNetRef = dotNetRef; + + // Tear down previous install completely + _teardownCheckboxes(); + + if (!annotations || annotations.length === 0) return; + + const ctx = { + annotations, + checkedIds: new Set(Array.isArray(checkedIds) ? checkedIds : []), + scrollEl: null, + pageEls: [], + resizeObs: null, + onScroll: null, + onResize: null, + resizeTimer: null, + }; + _installCtx = ctx; + + _waitForCheckboxPages(ctx); + } + + function _teardownCheckboxes() { + document.querySelectorAll('.annot-sig-cb-wrapper').forEach(el => el.remove()); + overlayButtons.clear(); + + if (!_installCtx) return; + const ctx = _installCtx; + + if (ctx.resizeObs) { ctx.resizeObs.disconnect(); ctx.resizeObs = null; } + if (ctx.scrollEl && ctx.onScroll) ctx.scrollEl.removeEventListener('scroll', ctx.onScroll); + if (ctx.onResize) window.removeEventListener('resize', ctx.onResize); + if (ctx.resizeTimer) clearTimeout(ctx.resizeTimer); + + _installCtx = null; + } + + function _waitForCheckboxPages(ctx) { + const wrapper = document.querySelector(VIEWER_WRAPPER_SEL); + if (!wrapper) return; + + if (!_tryInstallCheckboxes(ctx)) { + const observer = new MutationObserver(() => { + if (_tryInstallCheckboxes(ctx)) observer.disconnect(); + }); + observer.observe(wrapper, { childList: true, subtree: true }); + setTimeout(() => observer.disconnect(), 15000); + } + } + + function _tryInstallCheckboxes(ctx) { + const scrollEl = document.querySelector(SCROLL_CONTAINER_SEL); + if (!scrollEl) return false; + + const pageEls = Array.from(document.querySelectorAll(PAGE_IMG_SEL)); + if (pageEls.length === 0) return false; + + if (getComputedStyle(scrollEl).position === 'static') + scrollEl.style.position = 'relative'; + + ctx.scrollEl = scrollEl; + ctx.pageEls = pageEls; + + // Initial render + _renderAllCheckboxes(ctx); + + // ResizeObserver watches every page image for size changes (zoom in/out) + const ro = new ResizeObserver(() => _repositionAll(ctx)); + pageEls.forEach(el => ro.observe(el)); + ctx.resizeObs = ro; + + // Scroll reposition when user scrolls the viewer + ctx.onScroll = () => _repositionAll(ctx); + scrollEl.addEventListener('scroll', ctx.onScroll, { passive: true }); + + // Window resize debounced 60 ms + ctx.onResize = () => { + if (ctx.resizeTimer) clearTimeout(ctx.resizeTimer); + ctx.resizeTimer = setTimeout(() => _repositionAll(ctx), 60); + }; + window.addEventListener('resize', ctx.onResize, { passive: true }); + + return true; + } + + function _repositionAll(ctx) { + if (!ctx || !ctx.scrollEl) return; + + const scrollEl = ctx.scrollEl; + const pageEls = Array.from(document.querySelectorAll(PAGE_IMG_SEL)); // re-query in case DOM changed + if (pageEls.length === 0) return; + + const scrollRect = scrollEl.getBoundingClientRect(); + + ctx.annotations.forEach(ann => { + const wrapper = document.querySelector(`.annot-sig-cb-wrapper[data-annot-id="${ann.id}"]`); + if (!wrapper) return; + + const pageEl = pageEls[(ann.page || 1) - 1] ?? pageEls[pageEls.length - 1]; + if (!pageEl) return; + + const pageRect = pageEl.getBoundingClientRect(); + if (pageRect.width === 0 || pageRect.height === 0) return; + + const scaleX = pageRect.width / DX_PAGE_WIDTH; + const scaleY = pageRect.height / DX_PAGE_HEIGHT; + + const absLeft = pageRect.left - scrollRect.left + scrollEl.scrollLeft + (ann.x || 0) * scaleX; + const absTop = pageRect.top - scrollRect.top + scrollEl.scrollTop + (ann.y || 0) * scaleY; + const boxW = SIG_WIDTH_DX * scaleX; + const boxH = SIG_HEIGHT_DX * scaleY; + + wrapper.style.left = Math.round(absLeft) + 'px'; + wrapper.style.top = Math.round(absTop) + 'px'; + wrapper.style.width = Math.round(boxW) + 'px'; + wrapper.style.height = Math.round(boxH) + 'px'; + }); + } + + function _renderAllCheckboxes(ctx) { + const scrollEl = ctx.scrollEl; + const pageEls = ctx.pageEls; + const scrollRect = scrollEl.getBoundingClientRect(); + + ctx.annotations.forEach(ann => { + const pageEl = pageEls[(ann.page || 1) - 1] ?? pageEls[pageEls.length - 1]; + if (!pageEl) return; + + const pageRect = pageEl.getBoundingClientRect(); + if (pageRect.width === 0 || pageRect.height === 0) return; + + const scaleX = pageRect.width / DX_PAGE_WIDTH; + const scaleY = pageRect.height / DX_PAGE_HEIGHT; + + const absLeft = pageRect.left - scrollRect.left + scrollEl.scrollLeft + (ann.x || 0) * scaleX; + const absTop = pageRect.top - scrollRect.top + scrollEl.scrollTop + (ann.y || 0) * scaleY; + const boxW = SIG_WIDTH_DX * scaleX; + const boxH = SIG_HEIGHT_DX * scaleY; + + const isChecked = ctx.checkedIds.has(ann.id); + _createCheckboxOverlay(ann.id, scrollEl, absLeft, absTop, boxW, boxH, isChecked); + }); + } + + function _createCheckboxOverlay(annotationId, container, left, top, width, height, isChecked) { + const wrapper = document.createElement('div'); + wrapper.className = 'annot-sig-cb-wrapper' + (isChecked ? ' annot-sig-cb-wrapper--checked' : ''); + wrapper.setAttribute('data-annot-id', String(annotationId)); + + Object.assign(wrapper.style, { + position: 'absolute', + left: Math.round(left) + 'px', + top: Math.round(top) + 'px', + width: Math.round(width) + 'px', + height: Math.round(height) + 'px', + zIndex: '9999', + cursor: 'pointer', + boxSizing: 'border-box', + }); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = isChecked; + cb.className = 'annot-sig-cb'; + cb.setAttribute('aria-label', 'Unterschriftsfeld bestaetigen'); + + const label = document.createElement('span'); + label.className = 'annot-sig-cb__label'; + label.textContent = isChecked ? '\u2713 Bestaetigt' : '\u270e Hier unterschreiben'; + + wrapper.appendChild(cb); + wrapper.appendChild(label); + + wrapper.addEventListener('click', (e) => { + if (e.target !== cb) cb.checked = !cb.checked; + const checked = cb.checked; + wrapper.classList.toggle('annot-sig-cb-wrapper--checked', checked); + label.textContent = checked ? '\u2713 Bestaetigt' : '\u270e Hier unterschreiben'; + if (_installCtx) { + if (checked) _installCtx.checkedIds.add(annotationId); + else _installCtx.checkedIds.delete(annotationId); + } + if (_dotNetRef) + _dotNetRef.invokeMethodAsync('OnAnnotationToggled', annotationId, checked); + }); + + container.appendChild(wrapper); + overlayButtons.set(annotationId, { btn: wrapper, signed: isChecked }); + } + + // ?? Signature Pad ??????????????????????????????????????????????????????? + + function _pos(canvas, event) { + const r = canvas.getBoundingClientRect(); + const s = (event.touches && event.touches.length) ? event.touches[0] : event; + return { x: (s.clientX - r.left) * (canvas.width / r.width), y: (s.clientY - r.top) * (canvas.height / r.height) }; + } + + function _clear(canvas) { canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); } + + function initialize(canvasId) { + const canvas = document.getElementById(canvasId); + if (!canvas || pads.has(canvasId)) return; + const ctx = canvas.getContext('2d'); + ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = '#111'; + const state = { drawing: false, hasSignature: false }; + pads.set(canvasId, state); + const start = e => { e.preventDefault(); const p = _pos(canvas, e); state.drawing = true; ctx.beginPath(); ctx.moveTo(p.x, p.y); }; + const move = e => { if (!state.drawing) return; e.preventDefault(); const p = _pos(canvas, e); ctx.lineTo(p.x, p.y); ctx.stroke(); state.hasSignature = true; }; + const end = e => { if (!state.drawing) return; e.preventDefault(); state.drawing = false; }; + canvas.addEventListener('mousedown', start); + canvas.addEventListener('mousemove', move); + window.addEventListener('mouseup', end); + canvas.addEventListener('touchstart', start, { passive: false }); + canvas.addEventListener('touchmove', move, { passive: false }); + canvas.addEventListener('touchend', end, { passive: false }); + } + + function initializeTyped(canvasId) { + const canvas = document.getElementById(canvasId); + if (!canvas || typedSignatures.has(canvasId)) return; + typedSignatures.set(canvasId, { hasSignature: false }); + } + + function initializeImage(inputId, canvasId) { + const input = document.getElementById(inputId); + const canvas = document.getElementById(canvasId); + if (!input || !canvas || imageSignatures.has(canvasId)) return; + const state = { hasSignature: false }; + imageSignatures.set(canvasId, state); + input.addEventListener('change', () => { + const file = input.files && input.files[0]; + if (!file || !file.type.startsWith('image/')) { _clear(canvas); state.hasSignature = false; return; } + const reader = new FileReader(); + reader.onload = () => { + const img = new Image(); + img.onload = () => { + const ctx = canvas.getContext('2d'); _clear(canvas); + const p = 10, mw = canvas.width - p * 2, mh = canvas.height - p * 2; + const s = Math.min(mw / img.width, mh / img.height, 1); + ctx.drawImage(img, (canvas.width - img.width * s) / 2, (canvas.height - img.height * s) / 2, img.width * s, img.height * s); + state.hasSignature = true; + }; + img.src = reader.result; + }; + reader.readAsDataURL(file); + }); + } + + function clear(canvasId) { + const c = document.getElementById(canvasId); const s = pads.get(canvasId); + if (c && s) { _clear(c); s.hasSignature = false; } + } + + function clearTyped(canvasId) { + const c = document.getElementById(canvasId); const s = typedSignatures.get(canvasId); + if (c && s) { _clear(c); s.hasSignature = false; } + } + + function clearImage(inputId, canvasId) { + const inp = document.getElementById(inputId); const c = document.getElementById(canvasId); const s = imageSignatures.get(canvasId); + if (c && s) { if (inp) inp.value = ''; _clear(c); s.hasSignature = false; } + } + + function renderTypedSignature(canvasId, text, fontFamily) { + const canvas = document.getElementById(canvasId); const state = typedSignatures.get(canvasId); + if (!canvas || !state) return; + const value = (text || '').trim(); _clear(canvas); + if (!value) { state.hasSignature = false; return; } + const ctx = canvas.getContext('2d'); let fs = 54; + do { ctx.font = 'italic ' + fs + 'px ' + (fontFamily || 'cursive'); fs -= 2; } + while (ctx.measureText(value).width > canvas.width - 30 && fs > 24); + ctx.fillStyle = '#111'; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; + ctx.fillText(value, canvas.width / 2, canvas.height / 2); + state.hasSignature = true; + } + + function startTyped(elementId, text, typeSpeed) { + if (typeof Typed === 'undefined') return; + new Typed('#' + elementId, { + strings: [text], + typeSpeed: typeSpeed || 15, + showCursor: false + }); + } + + function getDataUrl(id) { const c = document.getElementById(id); const s = pads.get(id); return (c && s && s.hasSignature) ? c.toDataURL('image/png') : null; } + function getTypedDataUrl(id) { const c = document.getElementById(id); const s = typedSignatures.get(id); return (c && s && s.hasSignature) ? c.toDataURL('image/png') : null; } + function getImageDataUrl(id) { const c = document.getElementById(id); const s = imageSignatures.get(id); return (c && s && s.hasSignature) ? c.toDataURL('image/png') : null; } + + function loadExistingSignature(canvasId, dataUrl) { + const canvas = document.getElementById(canvasId); + const state = pads.get(canvasId); + if (!canvas || !state || !dataUrl) return; + + const ctx = canvas.getContext('2d'); + const img = new Image(); + + img.onload = () => { + _clear(canvas); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + state.hasSignature = true; + }; + + img.src = dataUrl; + } + + // ?? Public API ?????????????????????????????????????????????????????????? + return { + startTyped: startTyped, + installAnnotationCheckboxes: installAnnotationCheckboxes, + initialize: initialize, + initializeTyped: initializeTyped, + initializeImage: initializeImage, + clear: clear, + clearTyped: clearTyped, + clearImage: clearImage, + renderTypedSignature: renderTypedSignature, + getDataUrl: getDataUrl, + getTypedDataUrl: getTypedDataUrl, + getImageDataUrl: getImageDataUrl, + loadExistingSignature: loadExistingSignature + }; +})(); + diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/typed.umd.js b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/typed.umd.js new file mode 100644 index 00000000..0a4e41d6 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/typed.umd.js @@ -0,0 +1,3 @@ +!function(t,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(t||self).Typed=s()}(this,function(){function t(){return t=Object.assign?Object.assign.bind():function(t){for(var s=1;s0&&(e.strPos=e.currentElContent.length-1,e.strings.unshift(e.currentElContent)),e.sequence=[],e.strings)e.sequence[u]=u;e.arrayPos=0,e.stopNum=0,e.loop=e.options.loop,e.loopCount=e.options.loopCount,e.curLoop=0,e.shuffle=e.options.shuffle,e.pause={status:!1,typewrite:!0,curString:"",curStrPos:0},e.typingComplete=!1,e.autoInsertCss=e.options.autoInsertCss,e.autoInsertCss&&(this.appendCursorAnimationCss(e),this.appendFadeOutAnimationCss(e))},n.getCurrentElContent=function(t){return t.attr?t.el.getAttribute(t.attr):t.isInput?t.el.value:"html"===t.contentType?t.el.innerHTML:t.el.textContent},n.appendCursorAnimationCss=function(t){var s="data-typed-js-cursor-css";if(t.showCursor&&!document.querySelector("["+s+"]")){var e=document.createElement("style");e.setAttribute(s,"true"),e.innerHTML="\n .typed-cursor{\n opacity: 1;\n }\n .typed-cursor.typed-cursor--blink{\n animation: typedjsBlink 0.7s infinite;\n -webkit-animation: typedjsBlink 0.7s infinite;\n animation: typedjsBlink 0.7s infinite;\n }\n @keyframes typedjsBlink{\n 50% { opacity: 0.0; }\n }\n @-webkit-keyframes typedjsBlink{\n 0% { opacity: 1; }\n 50% { opacity: 0.0; }\n 100% { opacity: 1; }\n }\n ",document.body.appendChild(e)}},n.appendFadeOutAnimationCss=function(t){var s="data-typed-fadeout-js-css";if(t.fadeOut&&!document.querySelector("["+s+"]")){var e=document.createElement("style");e.setAttribute(s,"true"),e.innerHTML="\n .typed-fade-out{\n opacity: 0;\n transition: opacity .25s;\n }\n .typed-cursor.typed-cursor--blink.typed-fade-out{\n -webkit-animation: 0;\n animation: 0;\n }\n ",document.body.appendChild(e)}},e}()),n=new(/*#__PURE__*/function(){function t(){}var s=t.prototype;return s.typeHtmlChars=function(t,s,e){if("html"!==e.contentType)return s;var n=t.substring(s).charAt(0);if("<"===n||"&"===n){var i;for(i="<"===n?">":";";t.substring(s+1).charAt(0)!==i&&!(1+ ++s>t.length););s++}return s},s.backSpaceHtmlChars=function(t,s,e){if("html"!==e.contentType)return s;var n=t.substring(s).charAt(0);if(">"===n||";"===n){var i;for(i=">"===n?"<":"&";t.substring(s-1).charAt(0)!==i&&!(--s<0););s--}return s},t}());/*#__PURE__*/ +return function(){function t(t,s){e.load(this,s,t),this.begin()}var s=t.prototype;return s.toggle=function(){this.pause.status?this.start():this.stop()},s.stop=function(){this.typingComplete||this.pause.status||(this.toggleBlinking(!0),this.pause.status=!0,this.options.onStop(this.arrayPos,this))},s.start=function(){this.typingComplete||this.pause.status&&(this.pause.status=!1,this.pause.typewrite?this.typewrite(this.pause.curString,this.pause.curStrPos):this.backspace(this.pause.curString,this.pause.curStrPos),this.options.onStart(this.arrayPos,this))},s.destroy=function(){this.reset(!1),this.options.onDestroy(this)},s.reset=function(t){void 0===t&&(t=!0),clearInterval(this.timeout),this.replaceText(""),this.cursor&&this.cursor.parentNode&&(this.cursor.parentNode.removeChild(this.cursor),this.cursor=null),this.strPos=0,this.arrayPos=0,this.curLoop=0,t&&(this.insertCursor(),this.options.onReset(this),this.begin())},s.begin=function(){var t=this;this.options.onBegin(this),this.typingComplete=!1,this.shuffleStringsIfNeeded(this),this.insertCursor(),this.bindInputFocusEvents&&this.bindFocusEvents(),this.timeout=setTimeout(function(){0===t.strPos?t.typewrite(t.strings[t.sequence[t.arrayPos]],t.strPos):t.backspace(t.strings[t.sequence[t.arrayPos]],t.strPos)},this.startDelay)},s.typewrite=function(t,s){var e=this;this.fadeOut&&this.el.classList.contains(this.fadeOutClass)&&(this.el.classList.remove(this.fadeOutClass),this.cursor&&this.cursor.classList.remove(this.fadeOutClass));var i=this.humanizer(this.typeSpeed),r=1;!0!==this.pause.status?this.timeout=setTimeout(function(){s=n.typeHtmlChars(t,s,e);var i=0,o=t.substring(s);if("^"===o.charAt(0)&&/^\^\d+/.test(o)){var a=1;a+=(o=/\d+/.exec(o)[0]).length,i=parseInt(o),e.temporaryPause=!0,e.options.onTypingPaused(e.arrayPos,e),t=t.substring(0,s)+t.substring(s+a),e.toggleBlinking(!0)}if("`"===o.charAt(0)){for(;"`"!==t.substring(s+r).charAt(0)&&(r++,!(s+r>t.length)););var u=t.substring(0,s),p=t.substring(u.length+1,s+r),c=t.substring(s+r+1);t=u+p+c,r--}e.timeout=setTimeout(function(){e.toggleBlinking(!1),s>=t.length?e.doneTyping(t,s):e.keepTyping(t,s,r),e.temporaryPause&&(e.temporaryPause=!1,e.options.onTypingResumed(e.arrayPos,e))},i)},i):this.setPauseStatus(t,s,!0)},s.keepTyping=function(t,s,e){0===s&&(this.toggleBlinking(!1),this.options.preStringTyped(this.arrayPos,this));var n=t.substring(0,s+=e);this.replaceText(n),this.typewrite(t,s)},s.doneTyping=function(t,s){var e=this;this.options.onStringTyped(this.arrayPos,this),this.toggleBlinking(!0),this.arrayPos===this.strings.length-1&&(this.complete(),!1===this.loop||this.curLoop===this.loopCount)||(this.timeout=setTimeout(function(){e.backspace(t,s)},this.backDelay))},s.backspace=function(t,s){var e=this;if(!0!==this.pause.status){if(this.fadeOut)return this.initFadeOut();this.toggleBlinking(!1);var i=this.humanizer(this.backSpeed);this.timeout=setTimeout(function(){s=n.backSpaceHtmlChars(t,s,e);var i=t.substring(0,s);if(e.replaceText(i),e.smartBackspace){var r=e.strings[e.arrayPos+1];e.stopNum=r&&i===r.substring(0,s)?s:0}s>e.stopNum?(s--,e.backspace(t,s)):s<=e.stopNum&&(e.arrayPos++,e.arrayPos===e.strings.length?(e.arrayPos=0,e.options.onLastStringBackspaced(),e.shuffleStringsIfNeeded(),e.begin()):e.typewrite(e.strings[e.sequence[e.arrayPos]],s))},i)}else this.setPauseStatus(t,s,!1)},s.complete=function(){this.options.onComplete(this),this.loop?this.curLoop++:this.typingComplete=!0},s.setPauseStatus=function(t,s,e){this.pause.typewrite=e,this.pause.curString=t,this.pause.curStrPos=s},s.toggleBlinking=function(t){this.cursor&&(this.pause.status||this.cursorBlinking!==t&&(this.cursorBlinking=t,t?this.cursor.classList.add("typed-cursor--blink"):this.cursor.classList.remove("typed-cursor--blink")))},s.humanizer=function(t){return Math.round(Math.random()*t/2)+t},s.shuffleStringsIfNeeded=function(){this.shuffle&&(this.sequence=this.sequence.sort(function(){return Math.random()-.5}))},s.initFadeOut=function(){var t=this;return this.el.className+=" "+this.fadeOutClass,this.cursor&&(this.cursor.className+=" "+this.fadeOutClass),setTimeout(function(){t.arrayPos++,t.replaceText(""),t.strings.length>t.arrayPos?t.typewrite(t.strings[t.sequence[t.arrayPos]],0):(t.typewrite(t.strings[0],0),t.arrayPos=0)},this.fadeOutDelay)},s.replaceText=function(t){this.attr?this.el.setAttribute(this.attr,t):this.isInput?this.el.value=t:"html"===this.contentType?this.el.innerHTML=t:this.el.textContent=t},s.bindFocusEvents=function(){var t=this;this.isInput&&(this.el.addEventListener("focus",function(s){t.stop()}),this.el.addEventListener("blur",function(s){t.el.value&&0!==t.el.value.length||t.start()}))},s.insertCursor=function(){this.showCursor&&(this.cursor||(this.cursor=document.createElement("span"),this.cursor.className="typed-cursor",this.cursor.setAttribute("aria-hidden",!0),this.cursor.innerHTML=this.cursorChar,this.el.parentNode&&this.el.parentNode.insertBefore(this.cursor,this.el.nextSibling)))},t}()}); +//# sourceMappingURL=typed.umd.js.map diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json new file mode 100644 index 00000000..a2c36687 --- /dev/null +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json @@ -0,0 +1,39 @@ +{ + "ReverseProxy": { + "Routes": { + "auth-login": { + "ClusterId": "auth-hub", + "Match": { + "Path": "/api/auth", + "Methods": [ "POST" ] + }, + "Transforms": [ + { "PathSet": "/api/auth/sign-flow" } + ] + }, + "auth-envelope-receiver-login": { + "ClusterId": "auth-hub", + "Match": { + "Path": "/api/Auth/envelope-receiver/{key}", + "Methods": [ "POST" ] + }, + "Transforms": [ + { "PathPattern": "/api/auth/envelope-receiver/{key}" }, + { + "QueryValueParameter": "cookie", + "Set": "true" + } + ] + } + }, + "Clusters": { + "auth-hub": { + "Destinations": { + "primary": { + "Address": "http://172.24.12.39:9090" + } + } + } + } + } +} \ No newline at end of file diff --git a/EnvelopeGenerator.Server/publish.bat b/EnvelopeGenerator.Server/publish.bat new file mode 100644 index 00000000..b780f85a --- /dev/null +++ b/EnvelopeGenerator.Server/publish.bat @@ -0,0 +1,96 @@ +@echo off +setlocal + +echo ============================================================ +echo EnvelopeGenerator.Server - Self-Contained Publish +echo Target: win-x64 / .NET 8 / Release +echo ============================================================ +echo. + +REM Must be run from the solution root directory. +REM This file is located under: EnvelopeGenerator.Server\ + +set PROJECT=EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj +set OUTPUT=publish-output +set RID=win-x64 +set FRAMEWORK=net8.0 + +echo [1/3] Cleaning previous publish output... +if exist "%OUTPUT%" ( + rmdir /s /q "%OUTPUT%" + echo Removed: %OUTPUT% +) else ( + echo Nothing to clean. +) +echo. + +echo [2/3] Publishing... +echo Project : %PROJECT% +echo Output : %OUTPUT% +echo RID : %RID% +echo Framework: %FRAMEWORK% +echo. + +dotnet publish "%PROJECT%" ^ + -c Release ^ + -f %FRAMEWORK% ^ + --self-contained true ^ + --runtime %RID% ^ + -o "%OUTPUT%" + +if %ERRORLEVEL% neq 0 ( + echo. + echo [ERROR] Publish failed! ERRORLEVEL=%ERRORLEVEL% + pause + exit /b %ERRORLEVEL% +) + +echo. +echo [3/3] Verifying output... + +set PASS=1 + +if not exist "%OUTPUT%\EnvelopeGenerator.Server.exe" ( + echo [FAIL] EnvelopeGenerator.Server.exe not found! + set PASS=0 +) +if not exist "%OUTPUT%\hostfxr.dll" ( + echo [FAIL] hostfxr.dll not found! (Not a self-contained publish?) + set PASS=0 +) +if not exist "%OUTPUT%\coreclr.dll" ( + echo [FAIL] coreclr.dll not found! (Not a self-contained publish?) + set PASS=0 +) +if not exist "%OUTPUT%\Microsoft.Extensions.DependencyInjection.Abstractions.dll" ( + echo [FAIL] Microsoft.Extensions.DependencyInjection.Abstractions.dll not found! + set PASS=0 +) +if not exist "%OUTPUT%\web.config" ( + echo [FAIL] web.config not found! + set PASS=0 +) + +if "%PASS%"=="1" ( + echo. + echo ============================================================ + echo PUBLISH SUCCEEDED + echo Output folder: %~dp0%OUTPUT% + echo ============================================================ + echo. + echo Next steps: + echo 1. Copy the contents of '%OUTPUT%' to the IIS application directory + echo 2. Set the IIS Application Pool to 'No Managed Code' + echo 3. Recycle the Application Pool + echo. +) else ( + echo. + echo ============================================================ + echo PUBLISH COMPLETED BUT VERIFICATION FAILED + echo Review the FAIL messages above. + echo ============================================================ + echo. +) + +pause +endlocal diff --git a/EnvelopeGenerator.sln b/EnvelopeGenerator.sln index 2cc5d5b6..7d53a3cd 100644 --- a/EnvelopeGenerator.sln +++ b/EnvelopeGenerator.sln @@ -44,6 +44,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Dependenc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}" + ProjectSection(SolutionItems) = preProject + EnvelopeGenerator.Server\publish.bat = EnvelopeGenerator.Server\publish.bat + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj", "{4E6C54DA-576D-0955-2564-9EC890BB8279}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server.Client", "EnvelopeGenerator.Server\EnvelopeGenerator.Server.Client\EnvelopeGenerator.Server.Client.csproj", "{9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,6 +111,14 @@ Global {FB2D306B-1042-4A70-31ED-F991A1599371}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB2D306B-1042-4A70-31ED-F991A1599371}.Release|Any CPU.Build.0 = Release|Any CPU + {4E6C54DA-576D-0955-2564-9EC890BB8279}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E6C54DA-576D-0955-2564-9EC890BB8279}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E6C54DA-576D-0955-2564-9EC890BB8279}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E6C54DA-576D-0955-2564-9EC890BB8279}.Release|Any CPU.Build.0 = Release|Any CPU + {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +140,9 @@ Global {EC768913-6270-14F4-1DD3-69C87A659462} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} {5DCCF9A1-C03F-90E6-87D3-E96DB25250C2} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} {FB2D306B-1042-4A70-31ED-F991A1599371} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} + {BF1700D5-592E-4FFA-84E8-5480E289A1F0} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB} + {4E6C54DA-576D-0955-2564-9EC890BB8279} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0} + {9C41A0B1-6AFE-AFC5-CE33-08052F45DAEF} = {BF1700D5-592E-4FFA-84E8-5480E289A1F0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {73E60370-756D-45AD-A19A-C40A02DACCC7} diff --git a/RECEIVER_PDF_VIEWER_CONTEXT.md b/RECEIVER_PDF_VIEWER_CONTEXT.md new file mode 100644 index 00000000..6bb8360f --- /dev/null +++ b/RECEIVER_PDF_VIEWER_CONTEXT.md @@ -0,0 +1,1113 @@ +# EnvelopeGenerator Receiver PDF Viewer Context + +## Purpose +This document summarizes the active receiver-side PDF viewing and signing experience so that other agents can understand the current implementation quickly without re-reading all related files. + +This summary is based on the active host and current implementation in: +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css` + +--- + +## Active Page + +**Primary receiver page:** +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` + +**Route:** +- `/envelope/{EnvelopeKey}` + +**Render mode:** +- `InteractiveServer` + +This page is the active receiver PDF viewing and signing UI. + +--- + +## Core Architecture + +The receiver experience is built from three layers: + +### 1. Blazor page layer +`EnvelopeReceiverPage.razor` is responsible for: +- authorization flow +- loading receiver-specific document data +- loading signature placeholders +- loading and saving cached signature data +- UI state management +- toolbar interactions +- popup interactions +- JS interop orchestration + +### 2. PDF rendering layer +The active implementation currently uses `PDF.js` for: +- rendering the active PDF page +- rendering thumbnails +- handling zoom and page navigation state + +Referenced assets in the current implementation: +- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js` +- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css` + +### Planned target rendering layer +The main migration goal is to replace `PDF.js` in `EnvelopeReceiverPage.razor` with `DxPdfViewer` while preserving the complete receiver signing experience documented in this file. + +This means `DxPdfViewer` must become the main document rendering surface without regressing: +- single active page viewing behavior +- page navigation behavior +- zoom behavior +- thumbnail sidebar behavior +- receiver-specific signature placeholder overlays +- applied signature overlays +- signature navigation across pages +- overlay repositioning and resizing after zoom/page changes + +### 3. Custom enhancement layer +Extra behavior is implemented through custom JavaScript and CSS: +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css` + +These files extend the base PDF.js experience with receiver-specific features. + +--- + +## Main Functional Capabilities + +## 1. Single-page PDF viewing in the main viewer +The page shows one active PDF page at a time in the main canvas. + +Main viewer elements: +- `pdf-canvas` +- `pdf-text-layer` +- `pdf-signature-layer` + +This means the page is not using a continuous full-document scroll layout in the main area. Instead, it behaves like a single active page viewer with controlled navigation. + +--- + +## 2. Page navigation +The page supports multiple navigation methods: +- previous page button +- next page button +- direct page number input +- thumbnail click navigation +- signature navigation that may jump to another page automatically + +Relevant Blazor methods include: +- `PreviousPage()` +- `NextPage()` +- `OnPageInputChanged(...)` +- `GoToPageFromThumbnail(...)` +- `OnPageChangedBySignatureNav(...)` + +--- + +## 3. Zoom support +The page supports zooming in and out for the active PDF page. + +Available zoom behavior: +- zoom in button +- zoom out button +- zoom slider +- programmatic scale setting +- fit-to-width helper exists in code + +Relevant methods: +- `ZoomIn()` +- `ZoomOut()` +- `SetZoom(int percentage)` +- `OnZoomSliderChanged(...)` +- `FitToWidth()` +- `OnZoomChanged(double scale)` + +Current zoom constraints in the page: +- minimum: `50%` +- maximum: `300%` + +Important behavior: +- when zoom changes, signature placeholders and applied signature overlays are re-rendered/repositioned +- position and visual size are expected to stay synchronized with the current PDF scale + +--- + +## 4. Thumbnail sidebar +The page has a thumbnail sidebar for document page previews. + +Supported sidebar behavior: +- show/hide toggle +- click thumbnail to navigate to page +- highlight active page +- resizable width +- width persistence in `localStorage` + +Relevant state: +- `_showThumbnails` +- `_thumbnailWidth` +- `_isResizing` + +Relevant methods: +- `ToggleThumbnails()` +- `RenderThumbnailsAsync()` +- `OnSplitterMouseDown(...)` +- `OnSplitterMouseMove(...)` +- `OnSplitterMouseUp()` + +Persistence key: +- `envelopeViewer_thumbnailWidth` + +Important implementation detail: +- thumbnail rendering is done sequentially with delay to avoid overloading the browser + +--- + +## 5. Signature placeholder buttons on top of the PDF +The page places clickable signature buttons over the PDF for receiver-specific signature fields. + +Behavior: +- placeholders are loaded server-side +- placeholders are rendered on the client as overlay elements +- placeholders are shown for the current page +- clicking a placeholder applies the captured signature to that location + +Relevant Blazor method: +- `RenderSignatureButtonsAsync()` + +Relevant JS interop call: +- `pdfViewer.renderSignatureButtons` + +Important note: +- this is an overlay workflow, not direct PDF stamping +- placeholders belong to the current authenticated receiver + +--- + +## 6. Applying a real signature after clicking a placeholder +Once a receiver has a captured signature, clicking a signature placeholder applies the real signature overlay to the PDF viewer. + +Applied signature data includes: +- signature image data URL +- signer full name +- signer position +- place + +Relevant method: +- `OnSignatureButtonClick(int signatureId)` + +Relevant JS interop call: +- `pdfViewer.applySignature` + +Important note: +- the active implementation visually places the signature in the viewer layer +- the page is not currently doing final PDF binary stamping in this UI flow + +--- + +## 7. Signature navigation across all signature fields +The toolbar supports fast navigation between signature fields. + +Supported behavior: +- go to previous signature +- go to next signature +- maintain current signature index +- show signed / unsigned / total counts +- automatically move between pages if the next signature is on another page + +Relevant methods: +- `GoToPreviousSignature()` +- `GoToNextSignature()` +- `OnSignatureNavChanged()` +- `OnPageChangedBySignatureNav(int newPage)` +- `UpdateSignatureCounterAsync()` + +Relevant JS interop calls: +- `pdfViewer.goToPreviousSignature` +- `pdfViewer.goToNextSignature` +- `pdfViewer.getSignatureNavState` + +Counter state maintained in Blazor: +- `_totalSignatures` +- `_signedSignatures` +- `_unsignedSignatures` +- `_currentSignatureIndex` + +--- + +## 8. Automatic signature/button repositioning and resizing on zoom +A critical current feature is that zoom changes should keep overlay elements visually aligned with the document. + +This applies to: +- signature placeholder buttons +- applied signature overlays + +Expected behavior: +- position updates when zoom changes +- size updates when zoom changes +- re-render happens after page change and zoom change + +This behavior is triggered from methods such as: +- `OnZoomChanged(...)` +- `ZoomIn()` +- `ZoomOut()` +- `OnZoomSliderChanged(...)` +- `NextPage()` +- `PreviousPage()` +- `GoToPageFromThumbnail(...)` +- `OnPageChangedBySignatureNav(...)` + +In practice, the page repeatedly calls: +- `RenderSignatureButtonsAsync()` + +This is one of the key behaviors other agents must preserve. + +--- + +## 9. Signature capture popup +Signature creation is handled with a `DxPopup`. + +Popup capabilities: +- draw signature +- create typed signature +- upload signature image +- capture required signer metadata +- validate required fields before save + +Tabs: +- `draw` +- `text` +- `image` + +Relevant constants: +- `SignatureTabDraw` +- `SignatureTabText` +- `SignatureTabImage` + +Relevant canvas/input IDs: +- `envelope-signature-pad` +- `envelope-typed-signature-pad` +- `envelope-signature-image-input` +- `envelope-image-signature-pad` + +--- + +## 10. Signature capture modes +The current UI supports three signature creation modes. + +### Draw mode +Receiver signs directly on a canvas. + +Relevant JS usage: +- `receiverSignature.initialize` +- `receiverSignature.clear` +- `receiverSignature.getDataUrl` +- `receiverSignature.loadExistingSignature` + +### Text mode +Receiver enters a signature as text and chooses a font. + +Relevant JS usage: +- `receiverSignature.initializeTyped` +- `receiverSignature.renderTypedSignature` +- `receiverSignature.clearTyped` +- `receiverSignature.getTypedDataUrl` + +### Image mode +Receiver uploads an image of their signature. + +Relevant JS usage: +- `receiverSignature.initializeImage` +- `receiverSignature.clearImage` +- `receiverSignature.getImageDataUrl` + +--- + +## 11. Signature metadata requirements +The popup captures extra metadata besides the signature image. + +### Required +- full name +- place + +### Optional +- position + +Relevant bound fields: +- `_signerFullName` +- `_signaturePlace` +- `_signerPosition` + +Validation behavior in `SaveSignatureAsync()`: +- full name must not be empty +- place must not be empty +- signature image/data must not be empty + +--- + +## 12. Cached signature reuse +The page attempts to load a previously cached signature for the receiver. + +Behavior: +- if cache exists, the popup does not open automatically +- if cache does not exist, the popup opens on initial load +- saving a signature stores it back through the page data service + +Relevant service usage: +- `PageDataService.GetCachedSignatureAsync(...)` +- `PageDataService.SaveCachedSignatureAsync(...)` + +Related state: +- `_capturedSignature` +- `_signaturePopupVisible` + +Important note: +- the active cache handling is server-side through distributed cache +- the receiver signature is not only in browser state + +--- + +## 13. Signature change locking after signing starts +The current page prevents changing the captured signature after at least one signature has already been applied. + +Behavior: +- signature change button becomes disabled when `_signedSignatures > 0` +- title explains the signature is locked +- reset requires restarting the signing session + +Relevant methods: +- `GetSignatureButtonTitle()` +- `HandleSignatureChangeClick()` +- `RestartSigning()` + +Important implication: +- once actual field signing begins, the selected/captured signature is treated as fixed for that session unless the page is reset + +--- + +## 14. Reset/restart signing flow +The page includes a reset/restart behavior. + +Current behavior: +- page reload is used to reset signing UI state +- this clears current in-view applied signatures and session UI state + +Relevant method: +- `RestartSigning()` + +Implementation detail: +- current reset behavior is based on `Navigation.NavigateTo(Navigation.Uri, forceLoad: true)` + +--- + +## 15. PDF quality and rendering options +The page sends rendering options from .NET to JavaScript before viewer initialization. + +Relevant options include: +- thumbnail base scale +- thumbnail HiDPI support +- thumbnail max DPR +- main canvas HiDPI support +- main canvas max DPR +- smooth zoom enablement +- zoom transition duration +- rendering opacity +- zoom step percentage + +These come from: +- `IOptions` + +Relevant JS call: +- `pdfViewer.setQualityOptions(...)` + +This means rendering quality and viewer performance are configurable from server-side options. + +--- + +## 16. Local storage usage +The page currently uses `localStorage` at least for UI preferences. + +Known usage: +- thumbnail sidebar width persistence + +Known key: +- `envelopeViewer_thumbnailWidth` + +This is UI preference storage, not the main signature cache mechanism. + +--- + +## 17. Receiver authorization dependency +The page is not a public PDF viewer. It depends on receiver-specific authorization. + +Before loading the document: +- `ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey)` is executed +- unauthorized users are redirected to `/envelope/login/{EnvelopeKey}` + +Implication for other agents: +- viewer behavior is tied to authenticated receiver context +- placeholder loading and cached signature loading are receiver-specific + +--- + +## 18. Receiver-specific data loading +The page loads its data through `EnvelopeReceiverPageDataService`. + +Main server-side data loaded: +- document bytes +- signature placeholders +- receiver envelope data +- cached signature + +Important domain behavior: +- signature placeholders are filtered for the authenticated receiver +- placeholder coordinates are converted to points before UI use + +This matters for any future changes involving coordinate systems or overlay placement. + +--- + +## Important Non-Goals / Current Constraints + +### Not acceptable as a migration outcome +The migration is **not** successful if it results in: +- losing any receiver-side behavior documented in this file +- replacing the current workflow with a simplified read-only PDF viewer +- removing signature placeholder overlays or applied signature overlays +- removing cross-page signature navigation +- breaking zoom-time overlay synchronization +- falling back to legacy `ReceiverUI` as the main implementation target +- changing the flow into direct PDF binary stamping-only behavior in the main receiver UI + +### Current active model +The current implementation **is** based on: +- `EnvelopeGenerator.Server` +- `EnvelopeReceiverPage.razor` +- `PDF.js` +- custom overlay and JS interop behavior + +### Target model +The intended target model is: +- `EnvelopeGenerator.Server` +- `EnvelopeReceiverPage.razor` +- `DxPdfViewer` as the main PDF rendering component +- equivalent custom behavior for overlays, navigation, zoom synchronization, and receiver signing interactions +- preservation of the current receiver-specific authorization and page data loading flow + +--- + +## Key Behaviors Other Agents Must Preserve + +If another agent modifies this area, these behaviors are important to keep intact: + +1. Receiver authorization before document access +2. Single active page PDF viewing +3. Page navigation and thumbnail navigation +4. Zoom in/out with correct overlay synchronization +5. Signature placeholder rendering on the correct page +6. Clicking a placeholder applies the captured signature overlay +7. Fast previous/next signature navigation across pages +8. Automatic overlay position and size updates after zoom/page changes +9. Signature popup with draw/text/image modes +10. Required metadata validation for full name and place +11. Cached signature reuse +12. Signature lock after signing has started +13. Reset/restart signing behavior +14. Thumbnail width persistence + +## Migration Requirement + +Any migration from `PDF.js` to `DxPdfViewer` in `EnvelopeReceiverPage.razor` must be treated as a rendering engine swap, not a workflow redesign. + +The expected outcome is: +- `DxPdfViewer` replaces `PDF.js` as the main viewer technology +- the receiver authorization model remains unchanged +- the existing signature capture popup model remains unchanged +- signature placeholder rendering remains receiver-specific +- applied signature visuals remain aligned with the rendered document +- page navigation, zoom, thumbnails, and signature navigation continue to behave equivalently +- no documented feature in this file is dropped during the migration + +--- + +## Suggested Mental Model + +A useful way to think about the page is: + +- `EnvelopeReceiverPage.razor` = orchestration and state +- current rendering engine = `PDF.js` +- target rendering engine = `DxPdfViewer` +- current viewer behavior layer = `pdf-viewer.js` for overlays, navigation, thumbnails, resize logic +- `receiver-signature.js` = signature capture/creation logic +- server page data service = receiver-specific document and signature source + +--- + +## Detailed Technical Migration Plan: `PDF.js` -> `DxPdfViewer` + +This section records the implementation plan for migrating the active receiver signing experience from `PDF.js` to `DxPdfViewer` without losing any workflow behavior. + +### 1. Core migration principle + +This migration must be treated as: + +- a **rendering engine replacement** +- not a signing workflow redesign +- not a simplification to a read-only viewer +- not a direct PDF stamping rewrite + +The page-level orchestration in `EnvelopeReceiverPage.razor` remains the source of truth for: + +- receiver authorization +- document loading +- signature placeholder loading +- cached signature loading/saving +- popup and validation flow +- signature locking rules +- reset/restart behavior +- toolbar intent + +The rendering layer must be swapped while preserving those behaviors. + +### 2. Current implementation findings from code + +Based on the current files: + +- `EnvelopeReceiverPage.razor` +- `wwwroot/js/pdf-viewer.js` +- `wwwroot/css/envelope-viewer.css` +- `EnvelopeReceiverPage_DxPdfViewer.razor` + +the current page is tightly coupled to a custom `pdfViewer` JavaScript object. + +That object currently owns all viewer-engine behaviors: + +- PDF load and initialization +- current page state +- page rendering +- zoom rendering +- thumbnail rendering +- resize listener attachment +- signature button rendering +- applied signature overlay rendering +- signature navigation state + +This means the migration cannot be done by replacing only the Razor markup. The current JavaScript object is effectively both: + +- a `PDF.js` adapter +- and a receiver-signing overlay controller + +Those responsibilities must be separated conceptually during migration. + +### 3. Verified coordinate and viewport behavior from `PDF.js` + +Inspection of the `PDF.js` source comments and implementation confirms the following useful facts: + +1. `PageViewport` is created from: + - `viewBox` + - `scale` + - `rotation` + - optional offsets + +2. `PDF.js` explicitly documents that `PageViewport` creates a transform that converts: + - **PDF coordinate system** + - into **normal canvas-like coordinates** + +3. `PageViewportParameters.viewBox` is documented as: + - `xMin, yMin, xMax, yMax` + +4. `getViewport(...)` is documented as returning an object containing: + - `width` + - `height` + - transforms required for rendering + +5. `convertToViewportPoint(x, y)` is documented as converting: + - PDF coordinates + - into viewport coordinates + +6. `convertToPdfPoint(x, y)` is documented as converting: + - viewport coordinates + - back into PDF coordinates + - specifically useful for converting canvas pixel locations into PDF coordinates + +7. `rawDims` exposes unscaled page dimensions: + - `pageWidth` + - `pageHeight` + - `pageX` + - `pageY` + +8. For rotation `0`, `PageViewport` flips the Y axis into canvas-style coordinates unless `dontFlip` is used. + +### 4. Practical meaning of the current overlay math + +The current `pdf-viewer.js` implementation does **not** call `convertToViewportPoint(...)` directly. +Instead, it uses this simplified mapping: + +- `xPx = sig.x * scale` +- `yPx = sig.y * scale` + +This works only because the current upstream data contract already prepares signature coordinates in a way that matches the displayed page layout used by this app. + +From the wider workspace context, the receiver page data service already converts placeholder coordinates to `UnitOfLength.Point` before they reach the UI. + +Therefore, the active overlay contract is effectively: + +- incoming signature coordinates are already normalized for the receiver UI +- JavaScript currently assumes a top-left, page-relative, point-based overlay space +- visual pixel placement is obtained by multiplying by current display scale + +This is extremely important for the migration: + +- do **not** casually reinterpret existing signature coordinates as raw PDF bottom-left coordinates +- do **not** assume `DxPdfViewer` uses the same visible page coordinate origin +- preserve the effective contract already used by the receiver workflow + +### 5. Why the migration is technically hard + +Although both viewers display PDFs, they are fundamentally different integration surfaces. + +#### Current `PDF.js` model + +- low-level canvas rendering +- direct access to viewport scale +- direct control over single page rendering +- page DOM is owned by our code +- overlays are placed over known custom elements: + - `pdf-canvas` + - `pdf-text-layer` + - `pdf-signature-layer` + +#### Target `DxPdfViewer` model + +- component-driven render surface +- internal DOM is owned by DevExpress +- page layout lifecycle is controlled by the component +- zoom/page events may differ from `PDF.js` +- overlay host placement must be rediscovered or reintroduced + +So the migration is not a `canvas -> component` rename. +It is a **viewer capability remapping** problem. + +### 6. Technical target architecture + +The target architecture should be treated as four cooperating layers. + +#### 6.1 Blazor orchestration layer +Owned by `EnvelopeReceiverPage.razor`. + +Remains responsible for: + +- auth and redirect +- data loading +- signature popup state +- signature metadata validation +- cached signature reuse +- signed/unsigned counts +- restart behavior +- toolbar user intent + +#### 6.2 Viewer host layer +Main display surface becomes `DxPdfViewer`. + +This layer must provide equivalents for: + +- document load +- current page tracking +- total pages tracking +- page navigation +- zoom control +- layout change detection + +#### 6.3 Overlay adapter layer +Custom layer that translates page/zoom/layout information into overlay placement. + +This layer must handle: + +- signature placeholder buttons +- applied signature overlays +- page-relative visibility +- redraw after page/zoom/layout changes + +#### 6.4 Thumbnail/navigation support layer +Custom or hybrid support for: + +- left sidebar thumbnails +- active page highlight +- width persistence +- signature previous/next navigation + +### 7. Mandatory first implementation step: extract the current viewer contract + +Before changing behavior, the following current `pdfViewer` capabilities must be listed and then mapped one-by-one to the target implementation: + +- `initialize` +- `getTotalPages` +- `getCurrentPage` +- `nextPage` +- `previousPage` +- `goToPage` +- `zoomIn` +- `zoomOut` +- `setScale` +- `getScale` +- `fitToWidth` +- `renderThumbnail` +- `attachResizeListeners` +- `startResize` +- `renderSignatureButtons` +- `applySignature` +- `getSignatureNavState` +- `goToNextSignature` +- `goToPreviousSignature` +- `dispose` + +The migration should preserve this capability contract at the page level even if the internal engine changes. + +### 8. Planned migration strategy + +#### Phase 1 Confirm the `DxPdfViewer` integration surface + +Use `EnvelopeReceiverPage_DxPdfViewer.razor` as a reference and determine exactly what `DxPdfViewer` exposes for: + +- document content binding +- page navigation +- current page reading +- page change events +- zoom setting +- zoom change events +- page layout readiness +- DOM surface that can host overlays + +Key question: + +- can we reliably obtain visible page geometry from the live `DxPdfViewer` DOM? + +If yes, overlays can be positioned relative to the rendered page. +If no, a wrapper-based approximation or alternative integration is required. + +#### Phase 2 Replace the main render surface in `EnvelopeReceiverPage.razor` + +The current custom `canvas + text layer + signature layer` block should be replaced with a `DxPdfViewer` host region while keeping: + +- the same page route +- the same page state +- the same toolbar +- the same popup +- the same thumbnail sidebar shell + +At this stage, only the main document display needs to work. +Signature overlays may temporarily be disabled while the host geometry is established. + +#### Phase 3 Introduce a dedicated overlay host above `DxPdfViewer` + +The new viewer surface should be wrapped with a custom page container. + +Planned structure: + +- outer frame +- thumbnail sidebar +- splitter +- main viewer wrapper +- `DxPdfViewer` +- absolute-positioned custom overlay layer + +The overlay layer must be independently controlled by our code and not depend on internal `PDF.js` DOM ids. + +#### Phase 4 Rebuild page/zoom geometry acquisition + +Because the current implementation derives position using only `sig.x * scale`, the target implementation must determine: + +- currently visible page rectangle +- page top-left origin inside the wrapper +- effective display scale relative to logical point coordinates +- current page number + +At redraw time, the adapter must produce page-relative pixel coordinates for: + +- placeholder buttons +- applied signature blocks + +This should be centralized in one geometry function rather than scattered across multiple viewer actions. + +#### Phase 5 Move signature overlay state to a canonical model + +The current JavaScript keeps runtime state in: + +- `signatureButtons` +- `appliedSignatures` +- `appliedSignatureElements` +- `_allSignatures` +- `_lastViewedSignatureId` + +This must remain stable across redraws. + +Preferably: + +- Blazor continues to own the authoritative business state +- JavaScript owns transient rendered DOM state only + +If necessary, applied signature state should be re-sendable from .NET after viewer redraw. +That prevents losing visual signatures when page layout changes. + +#### Phase 6 Re-implement signature placeholder rendering on top of `DxPdfViewer` + +The current implementation filters placeholders by: + +- current page +- not already applied + +That same behavior must remain. + +Rendering rules to preserve: + +- only placeholders for the active page are shown +- already applied placeholders are hidden as buttons +- button size grows/shrinks with zoom +- button click invokes `.NET` callback `OnSignatureButtonClick` + +The existing scaling behavior uses `baseScale = 1.5` as the visual reference. +That visual convention should be preserved initially unless a new normalized sizing model is intentionally introduced. + +#### Phase 7 Re-implement applied signature rendering on top of `DxPdfViewer` + +The current applied signature overlay includes: + +- signature image +- horizontal separator line +- signer full name +- optional position +- place and date + +The same visual block must be retained. + +Required behavior: + +- clicking a placeholder converts it to an applied signature overlay +- applied signature remains visible on the correct page +- applied signature rescales with zoom +- applied signature repositions on page/zoom changes +- applied signature is hidden when the user navigates to a different page + +#### Phase 8 Re-implement page navigation through a central page-change pipeline + +The following actions must all converge into a single page navigation routine: + +- previous page button +- next page button +- direct page number input +- thumbnail click +- signature previous/next navigation when target is on another page + +That routine must do all of the following in order: + +1. instruct viewer to change page +2. update canonical current page state +3. wait for rendered page surface readiness if needed +4. redraw placeholder overlays +5. refresh signature navigation counter state + +#### Phase 9 Re-implement zoom through a central zoom-change pipeline + +The following actions must converge into one zoom update path: + +- toolbar zoom in +- toolbar zoom out +- slider change +- fit-to-width +- viewer-native zoom events if the user triggers zoom through gestures + +That routine must: + +1. clamp to `50% - 300%` +2. update canonical zoom state +3. wait for layout/render completion if needed +4. redraw placeholder overlays +5. rescale applied signatures + +The current implementation already treats redraw after zoom as mandatory. That must remain true. + +#### Phase 10 Preserve signature navigation independently from the viewer engine + +Current navigation logic is driven by the global ordered signature list and not by PDF rendering internals alone. + +This behavior must remain engine-independent: + +- previous/next traverses the full signature list +- traversal wraps around at the edges +- if the target signature is on another page, the viewer jumps first +- after page jump, the target element is scrolled into view +- toolbar counters reflect total/signed/unsigned/current index + +If `DxPdfViewer` changes scrolling mechanics, the `scrollToElement` / `scrollToButton` logic must be adapted, but the traversal rules must remain unchanged. + +#### Phase 11 Thumbnail sidebar should remain custom unless `DxPdfViewer` can match all current behavior + +The current sidebar is not just decorative. It also provides: + +- show/hide toggle +- click navigation +- active page highlight +- resizable width +- width persistence in `localStorage` +- progressive rendering strategy + +Because of that, the safest plan is: + +- keep the custom sidebar shell and resize behavior +- only replace the source of main document rendering + +If `DxPdfViewer` cannot provide matching thumbnails directly, a hybrid solution is acceptable: + +- main document rendered by `DxPdfViewer` +- thumbnails still generated through a separate custom preview pipeline + +This still satisfies the requirement that `DxPdfViewer` becomes the main viewer technology. + +#### Phase 12 Keep signature capture popup unchanged unless integration forces a minimal adjustment + +`receiver-signature.js` and the popup flow should remain mostly untouched. + +Preserve exactly: + +- draw tab +- text tab +- image tab +- full name required +- place required +- signature data required +- cached signature reuse +- signature change lock after signing starts + +This area is low-risk and should not be refactored unnecessarily during the viewer migration. + +### 9. File-by-file execution plan + +#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` + +Planned work: + +- remove `PDF.js` CDN asset dependency from the active receiver page +- replace the custom canvas-based main surface with a `DxPdfViewer` host +- preserve toolbar, popup, auth, state and receiver-specific loading logic +- redirect viewer method calls through a new or adapted JS interop surface +- preserve current state fields wherever possible to minimize workflow regressions + +#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js` + +Planned work: + +- split engine-specific logic from workflow-specific logic +- remove dependency on direct `pdf-canvas` rendering for the main viewer path +- introduce `DxPdfViewer`-compatible geometry and overlay placement helpers +- preserve signature navigation logic semantics +- preserve overlay rendering semantics +- preserve resize integration for sidebar handling + +This file will likely become a viewer adapter rather than a pure `PDF.js` implementation. + +#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css` + +Planned work: + +- keep page shell, toolbar, thumbnails, splitter and popup-related styles +- replace canvas/text-layer specific assumptions with viewer-wrapper styles +- introduce overlay host styles for the `DxPdfViewer` surface +- ensure z-index ordering still makes signature buttons clickable +- avoid CSS leaking into DevExpress internal DOM more than necessary + +#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` + +Planned role: + +- reference only +- used to understand document binding and minimal `DxPdfViewer` setup +- not the primary delivery target unless portions are borrowed into the main receiver page + +### 10. Risks that must be explicitly tested during implementation + +1. `DxPdfViewer` may not expose visible page geometry directly. +2. `DxPdfViewer` may render asynchronously in a way that requires delayed overlay redraw. +3. Built-in zoom/page state may not map 1:1 to current custom toolbar assumptions. +4. Sidebar width changes may require explicit overlay recalculation. +5. Applied signatures may visually drift if scaling math is not centralized. +6. Signature navigation may fail if target DOM nodes are created later than expected. +7. Built-in viewer scrolling may differ from the current wrapper scrolling model. + +### 11. Acceptance checklist for the migration + +The migration is only acceptable if all of the following still work in the active page: + +- receiver authorization before document access +- document load for the authenticated receiver +- single active page viewing +- previous/next page navigation +- direct page input navigation +- thumbnail click navigation +- zoom in/out and slider zoom +- overlay alignment after zoom changes +- signature placeholder rendering on the correct page +- placeholder click applying signature overlay +- previous/next signature navigation across pages +- signed/unsigned counters updating correctly +- cached signature reuse +- signature popup validation +- signature change lock after signing starts +- restart signing behavior +- thumbnail width persistence + +### 12. Final implementation guidance + +The correct technical approach is: + +- keep `EnvelopeReceiverPage.razor` as the receiver workflow orchestrator +- treat `DxPdfViewer` as the new main rendering engine +- rebuild overlay placement as a viewer-agnostic custom layer +- preserve the current receiver-specific state machine +- preserve signature navigation rules +- preserve thumbnail/sidebar interaction model where practical + +In short: + +- **replace the renderer** +- **preserve the workflow** +- **rebuild the overlay adapter** +- **do not redesign the signing experience** + +--- + +## Files Most Likely Relevant For Future Work + +### Main page +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` + +### JavaScript +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js` + +### CSS +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css` + +### Data/auth services +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs` +- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs` + +--- + +## Summary +The active receiver document experience is currently a `PDF.js`-based, server-orchestrated, overlay-signing workflow in `EnvelopeGenerator.Server`. + +It supports: +- single-page PDF display +- page navigation +- zoom +- thumbnails +- receiver-specific signature placeholders +- actual signature overlay placement +- fast signature navigation across pages +- overlay rescaling/repositioning during zoom +- signature capture via draw/text/image +- cached signature reuse + +The current migration goal is to move this experience to `DxPdfViewer` in `EnvelopeReceiverPage.razor` without losing any of the capabilities listed above. + +Any future changes in this area should be made with the assumption that this is a custom receiver signing experience whose rendering engine may change, but whose functional behavior must remain intact; it is not a basic document embed and not a direct PDF stamping pipeline.