Initialize Blazor SSR project structure

This commit is contained in:
Beau Findlay
2026-01-30 22:20:41 +00:00
parent 498e5a6080
commit 361f6212bd
65 changed files with 60948 additions and 0 deletions

59
.gitignore vendored Normal file
View File

@@ -0,0 +1,59 @@
## .NET
bin/
obj/
*.user
*.suo
*.userosscache
*.sln.docstates
.vs/
.vscode/
## Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
## Visual Studio cache/options directory
.vs/
.vscode/
## JetBrains Rider
.idea/
*.sln.iml
## User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
## ASP.NET Scaffolding
ScaffoldingReadMe.txt
## Publish profiles
*.pubxml
*.azurePubxml
## NuGet Packages
*.nupkg
*.snupkg
**/packages/*
## Node.js (keeping for now, will remove after React cleanup)
node_modules/
package-lock.json
## OS files
.DS_Store
Thumbs.db

766
migration.md Normal file
View File

@@ -0,0 +1,766 @@
# Blazor SSR Migration Plan
## Project Overview
Migrating a React/TypeScript portfolio website to a **minimal, dependency-free** .NET Blazor application with static server-side rendering.
**Current Stack:**
- React 18.2 + TypeScript
- Vite build tool
- Tailwind CSS (to be replaced with vanilla CSS)
- React Router DOM
- Headless UI components (Dialog/Tabs - to be replaced with vanilla HTML/CSS/JS)
- React Icons (to be replaced with SVG icons)
- Azure Static Web Apps hosting
**Target Stack:**
- .NET 10 Blazor with Static SSR (no WebSockets/SignalR)
- C# Razor components (server-rendered only)
- **Vanilla CSS only** (no frameworks, no preprocessors)
- Blazor Router (built-in)
- **No external component libraries**
- **No icon dependencies** (inline SVG)
- Azure App Service or Azure Container Apps hosting
**Migration Philosophy:**
- Zero external dependencies beyond .NET runtime
- Simple, maintainable vanilla CSS
- Static server-side rendering (no interactivity circuits)
- Modern CSS features (Grid, Flexbox, CSS Variables, Container Queries)
- Native HTML elements with custom styling
- **Zero JavaScript** - pure CSS solutions for all interactivity (checkbox hack, :target pseudo-class)
---
## Phase 1: Project Setup
### 1.1 .NET Project Initialization
- [ ] Install .NET 10 SDK
- [ ] Create new Blazor Web App project with SSR only: `dotnet new blazor -o src/BlazorApp -int None`
- Note: `-int None` disables interactivity (no SignalR/WebSockets)
- [ ] Verify project builds: `dotnet build`
- [ ] Review generated files: `Program.cs`, `App.razor`, `Components/_Imports.razor`, `appsettings.json`
- [ ] Confirm `Program.cs` does NOT include `AddInteractiveServerComponents()` or SignalR setup
### 1.2 Solution Structure
- [ ] Create solution file: `dotnet new sln -n my-portfolio`
- [ ] Add project to solution: `dotnet sln add src/BlazorApp/BlazorApp.csproj`
- [ ] Configure project properties (nullable, implicit usings)
- [ ] Ensure render modes are set to static SSR only
### 1.3 Git Configuration
- [ ] Update `.gitignore` for .NET (bin/, obj/, .vs/)
- [ ] Archive React project (move to `archive/react-version/` or create `pre-blazor-migration` tag)
- [ ] Commit initial Blazor project structure
---
## Phase 2: CSS Architecture & Design System
### 2.1 CSS Variables Setup
- [ ] Create `wwwroot/css/variables.css` with design tokens:
- Colors (black: #000, white: #fff, gray shades)
- Spacing scale (0.25rem, 0.5rem, 1rem, 1.5rem, 2rem, etc.)
- Typography (font-family: monospace, font-sizes, line-heights)
- Breakpoints (mobile-first: 640px, 768px, 1024px, 1280px)
- Z-index scale
- Border radius values
- Transition durations
### 2.2 Base Styles
- [ ] Create `wwwroot/css/reset.css` with modern CSS reset
- [ ] Create `wwwroot/css/base.css`:
- Body styles (background: black, color: slate-50, font: monospace)
- Typography defaults
- Link styles
- Focus styles for accessibility
- Custom scrollbar styles
- Selection styles
### 2.3 Layout Utilities
- [ ] Create `wwwroot/css/layout.css`:
- Container classes (max-width, centering)
- Flexbox utilities (flex, flex-col, items-center, justify-between, gap, etc.)
- Grid utilities (grid, grid-cols)
- Spacing utilities (padding, margin classes)
- Responsive utilities
### 2.4 Component Styles
- [ ] Create `wwwroot/css/components.css`:
- Button styles (primary, secondary, hover, focus, disabled states)
- Input/textarea styles
- Card styles
- Navigation styles
- Footer styles
- Link styles
- [ ] Create `wwwroot/css/animations.css`:
- Fade-in animation
- Hover transitions
- Loading spinner animation
### 2.5 Compile CSS
- [ ] Create `wwwroot/css/app.css` that imports all CSS files
- [ ] Reference in `index.html`: `<link href="css/app.css" rel="stylesheet" />`
- [ ] Test CSS loads correctly
---
## Phase 3: SVG Icon System
### 3.1 Extract Icons
- [ ] Identify all react-icons used:
- FaBars (hamburger menu)
- FaXmark (close X)
- FaGithub
- FaLinkedin
- FaEnvelope
- FaDatabase
- FaDocker
- FaReact
- SiCsharp
- SiMicrosoftazure
- SiBlazor
- [ ] Download SVG paths from icon sources (FontAwesome, Simple Icons)
- [ ] Create reusable Icon component: `Components/Icon.razor`
### 3.2 Icon Component
- [ ] Implement `Icon.razor` with parameters:
- `Name` (string): icon identifier
- `Size` (int, default 24): icon size in pixels
- `CssClass` (string): additional CSS classes
- [ ] Store SVG paths in C# dictionary or switch statement
- [ ] Test all icons render correctly
---
## Phase 4: Core Application Structure
### 4.1 Routing Setup
- [ ] Configure routes in `App.razor`:
- `/` → Home page
- `/work` → Work page
- `/about` → About page
- `NotFound` → 404/Error page
- [ ] Test routing and navigation
### 4.2 Layout Component
- [ ] Create `Shared/MainLayout.razor`
- [ ] Implement structure:
- Header with NavBar
- Main content area with `@Body`
- Footer
- [ ] Apply CSS classes (container, flex layout, min-height)
- [ ] Add fade-in animation class
- [ ] Test layout renders correctly
### 4.3 Navigation Component
- [ ] Create `Shared/NavBar.razor`
- [ ] Implement desktop navigation:
- Logo image
- Navigation links (Home, Work, About)
- [ ] Implement mobile navigation:
- Hamburger button (toggle mobile menu)
- Mobile menu overlay with links
- Close button
- Social icons in mobile menu
- [ ] Use Blazor's built-in `NavLink` component with custom styling
- [ ] Implement mobile menu state with C# boolean property
- [ ] Style with pure CSS (no Headless UI)
### 4.4 Footer Component
- [ ] Create `Shared/Footer.razor`
- [ ] Port footer content
- [ ] Apply styling
---
## Phase 5: Page Components
### 5.1 Home Page
- [ ] Create `Pages/Home.razor` with `@page "/"`
- [ ] Port content from `HomePage.tsx`
- [ ] Reference child components (Title, Text, TechIcons)
- [ ] Test rendering
### 5.2 Work Page
- [ ] Create `Pages/Work.razor` with `@page "/work"`
- [ ] Port content from `WorkPage.tsx`
- [ ] Test rendering
### 5.3 About Page
- [ ] Create `Pages/About.razor` with `@page "/about"`
- [ ] Port content from `AboutPage.tsx`
- [ ] Test rendering
### 5.4 Error/404 Page
- [ ] Create `Pages/NotFound.razor` with `@page "/404"`
- [ ] Port content from `ErrorPage.tsx`
- [ ] Configure as NotFound in router
- [ ] Test 404 handling
---
## Phase 6: Reusable UI Components
### 6.1 Typography Components (7)
- [ ] `Components/Title.razor` - H1 heading
- [ ] `Components/Subtitle.razor` - H2 heading
- [ ] `Components/Text.razor` - Paragraph with margin
- [ ] `Components/Label.razor` - Form label
- [ ] `Components/List.razor` - UL wrapper
- [ ] `Components/ListItem.razor` - LI element
- [ ] `Components/AnchorLink.razor` - External link with styling
### 6.2 Form Components (3)
- [ ] `Components/Button.razor` - Button with hover/focus states
- [ ] `Components/TextInput.razor` - Input field with label
- [ ] `Components/TextAreaInput.razor` - Textarea with label
### 6.3 Display Components (5)
- [ ] `Components/TechIcons.razor` - Tech stack grid with icons
- [ ] `Components/SocialIcons.razor` - Social media links with icons
- [ ] `Components/WorkTimeline.razor` - Work experience timeline
- [ ] `Components/Loading.razor` - Loading state wrapper
- [ ] `Components/LoadingSpinner.razor` - Spinner animation
### 6.4 Feature Components (2)
- [ ] `Components/ContactMe.razor` - Contact form
- [ ] `Components/AboutTabs.razor` - Tab interface (vanilla CSS/JS)
---
## Phase 7: Interactive Components (Pure CSS - No JavaScript)
### 7.1 Mobile Menu (CSS Checkbox Hack)
- [ ] Implement in `NavBar.razor`:
- Add hidden checkbox: `<input type="checkbox" id="mobile-menu-toggle" class="menu-toggle" />`
- Add label for hamburger: `<label for="mobile-menu-toggle">☰</label>`
- Add label for close button inside menu: `<label for="mobile-menu-toggle">✕</label>`
- Render menu panel as sibling to checkbox
- [ ] Style with CSS:
- Hide checkbox: `.menu-toggle { display: none; }`
- Show/hide menu based on checkbox state: `.menu-toggle:checked ~ .mobile-menu { ... }`
- Show/hide overlay: `.menu-toggle:checked ~ .overlay { ... }`
- Slide-in animation using `transform: translateX()`
- Transparent overlay with backdrop-filter blur
- Z-index layering for proper stacking
- [ ] Test keyboard navigation (checkbox is focusable)
### 7.2 Tabs Component (CSS :target Pseudo-Class or Radio Buttons)
- [ ] **Option A - Radio Button Approach (Recommended):**
- Create `AboutTabs.razor`:
- Hidden radio inputs: `<input type="radio" name="tabs" id="tab1" checked />`
- Label buttons: `<label for="tab1">Tab 1</label>`
- Tab panels as siblings to inputs
- Style with CSS:
- Hide radio buttons: `input[type="radio"] { display: none; }`
- Active tab styling: `#tab1:checked ~ .tabs-labels label[for="tab1"] { ... }`
- Show panel: `#tab1:checked ~ .tab-panels .panel1 { display: block; }`
- Hide other panels by default
- [ ] **Option B - :target Pseudo-Class:**
- Use anchor links: `<a href="#tab1">Tab 1</a>`
- Panel IDs: `<div id="tab1" class="tab-panel">...</div>`
- Style: `.tab-panel:target { display: block; }`
- Note: Changes URL hash
- [ ] Choose approach and implement
- [ ] Add smooth transitions with CSS
### 7.3 Form Handling (Traditional POST)
- [ ] Implement form submission in `ContactMe.razor`
- [ ] Use standard HTML `<form>` with POST action
- [ ] Server-side endpoint to handle form submission
- [ ] Redirect after post pattern (PRG)
- [ ] Add HTML5 validation attributes
- [ ] Style validation states with CSS (`:invalid`, `:valid`)
---
## Phase 8: Tailwind CSS to Vanilla CSS Conversion
### 8.1 Analyze Tailwind Usage
- [ ] Document all Tailwind classes used in current app:
- Layout: `flex`, `flex-col`, `items-center`, `justify-between`, `max-w-7xl`, etc.
- Spacing: `px-6`, `py-8`, `mt-4`, `mb-6`, `space-x-6`, etc.
- Typography: `text-sm`, `text-xl`, `font-semibold`, `font-mono`, etc.
- Colors: `bg-black`, `text-white`, `text-gray-200`, `ring-gray-300`, etc.
- Responsive: `lg:flex`, `md:order-2`, `sm:max-w-sm`, etc.
- Effects: `hover:bg-gray-800`, `focus-visible:outline`, etc.
### 8.2 Create CSS Equivalents
- [ ] Create utility classes in `layout.css`:
```css
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.max-w-7xl { max-width: 80rem; margin: 0 auto; }
.container { max-width: 1280px; margin: 0 auto; padding: 0 1.5rem; }
```
- [ ] Create spacing utilities:
```css
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.mt-4 { margin-top: 1rem; }
/* etc. */
```
- [ ] Create responsive utilities using media queries:
```css
@media (min-width: 1024px) {
.lg\:flex { display: flex; }
}
```
- [ ] Alternative: Use component-specific classes instead of utilities
### 8.3 Choose Approach
- [ ] **Option A (Recommended):** Component-specific CSS classes
- More maintainable for small projects
- Better for this portfolio site (20 components)
- Example: `.nav-bar {}`, `.nav-bar__logo {}`, `.nav-bar__menu {}`
- [ ] **Option B:** Minimal utility classes
- Create only the most-used utilities (flex, grid, spacing)
- Combine with component classes
- [ ] Document chosen approach in README
---
## Phase 9: SEO & Meta Tags
### 9.1 HTML Head Configuration
- [ ] Update `wwwroot/index.html` with meta tags:
- Charset, viewport
- Description, author
- Open Graph tags (og:title, og:description, og:image, og:url)
- Favicon reference
- [ ] Ensure all tags from React version are migrated
- [ ] Test with social media preview tools
### 9.2 Analytics Integration
- [ ] Add Google Analytics script to `index.html`
- [ ] Add CookieYes consent script to `index.html`
- [ ] Test analytics on navigation (use `NavigationManager.LocationChanged` if needed)
- [ ] Verify GDPR compliance
---
## Phase 10: Build & Optimization
### 10.1 Build Configuration
- [ ] Configure `.csproj` for optimizations:
```xml
<PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishReadyToRunShowWarnings>true</PublishReadyToRunShowWarnings>
</PropertyGroup>
```
- [ ] Test production build: `dotnet publish -c Release`
- [ ] Analyze output in `bin/Release/net10.0/publish/`
- [ ] Verify CSS is minimal and unminified (easy to debug)
- [ ] Verify NO JavaScript files present
### 10.2 CSS Optimization
- [ ] Remove unused CSS
- [ ] Combine CSS files if beneficial
- [ ] Consider minification (optional - Azure handles this)
- [ ] Test CSS loads and applies correctly
- [ ] Verify CSS-only menu and tabs work in all browsers
### 10.3 Image Optimization
- [ ] Verify `logo.webp` is optimized
- [ ] Add width/height attributes to prevent layout shift
- [ ] Test image loading
### 10.4 Zero JavaScript Verification
- [ ] Confirm no `.js` files in `wwwroot/js/`
- [ ] Confirm no `<script>` tags in layouts/pages
- [ ] Test site functions without JavaScript enabled in browser
---
## Phase 11: Deployment Configuration
### 11.1 Azure App Service Configuration
- [ ] Create Azure App Service (Linux or Windows)
- [ ] Configure environment variables in Azure portal:
- `ASPNETCORE_ENVIRONMENT=Production`
- [ ] Enable HTTPS only
- [ ] Configure health check endpoint
- [ ] Set up Application Insights (optional)
### 11.2 GitHub Actions Workflow
- [ ] Create/update `.github/workflows/azure-deploy.yml`:
```yaml
name: Deploy to Azure App Service
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '10.0.x'
- name: Build
run: dotnet build src/BlazorApp/BlazorApp.csproj -c Release
- name: Publish
run: dotnet publish src/BlazorApp/BlazorApp.csproj -c Release -o ${{env.DOTNET_ROOT}}/myapp
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: 'your-app-name'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ${{env.DOTNET_ROOT}}/myapp
```
- [ ] Set up Azure publish profile in GitHub secrets
- [ ] Test workflow on branch
### 11.3 Local Development Setup
- [ ] Document commands:
- Run: `dotnet watch` (hot reload enabled)
- Build: `dotnet build`
- Publish: `dotnet publish -c Release`
- [ ] Test hot reload functionality
- [ ] Configure HTTPS for local dev (trust cert)
- [ ] Verify no SignalR scripts loaded (check browser network tab)
- [ ] Test CSS-only interactivity (menu, tabs) during development
---
## Phase 12: Testing & Quality Assurance
### 12.1 Visual Testing
- [ ] Compare React site vs Blazor site side-by-side
- [ ] Test all pages render identically
- [ ] Test all components display correctly
- [ ] Test responsive design:
- Mobile (320px, 375px, 414px)
- Tablet (768px, 1024px)
- Desktop (1280px, 1920px)
- [ ] Test cross-browser:
- Chrome/Edge (Chromium)
- Firefox
- Safari (if available)
### 12.2 Functional Testing
- [ ] Test navigation between pages (full page reloads)
- [ ] Test mobile menu open/close (CSS checkbox)
- [ ] Test tab switching in About page (CSS radio/target)
- [ ] Test all external links
- [ ] Test form submission with POST/redirect
- [ ] Test 404 page navigation
- [ ] Test with JavaScript disabled in browser
- [ ] Verify no JavaScript errors (should be none)
### 12.3 Accessibility Testing
- [ ] Test keyboard navigation (Tab, Enter, Space for checkboxes/radios)
- [ ] Test screen reader compatibility (aria-labels, proper label associations)
- [ ] Test focus indicators (especially on hidden checkbox/radio controls)
- [ ] Test color contrast (black/white theme)
- [ ] Verify checkbox and radio button labels are properly associated
- [ ] Run Lighthouse accessibility audit
### 12.4 Performance Testing
- [ ] Test server response times for page requests
- [ ] Test First Contentful Paint (FCP)
- [ ] Test Time to Interactive (TTI)
- [ ] Test with throttled network (3G)
- [ ] Verify no WebSocket connections in network tab
- [ ] Verify no JavaScript downloads
- [ ] Run Lighthouse performance audit
- [ ] Target: FCP < 1s, TTI < 1.5s (server-rendered, zero JavaScript, instant interactive)
### 12.5 SEO Testing
- [ ] Test meta tags with social media debuggers:
- Facebook Sharing Debugger
- Twitter Card Validator
- LinkedIn Post Inspector
- [ ] Test robots.txt accessibility
- [ ] Test favicon in browser tabs
- [ ] Run Lighthouse SEO audit
---
## Phase 13: Documentation & Cleanup
### 13.1 Update Documentation
- [ ] Update README.md:
```markdown
# beaufindlay.com
My personal portfolio site built with .NET Blazor WebAssembly.
## Tech Stack
- .NET 10 Blazor SSR (static server-side rendering)
- Pure CSS only (zero JavaScript)
- Hosted on Azure App Service
## Development
- Run: `dotnet watch`
- Build: `dotnet build`
- Publish: `dotnet publish -c Release`
## Features
- Zero external dependencies (no npm, no frameworks, no JavaScript)
- Static server-side rendering (no WebSockets)
- Pure CSS interactivity (checkbox hack for menu, radio buttons for tabs)
- Mobile-first responsive design
- Accessible and SEO-optimized
- Works with JavaScript disabled
```
- [ ] Document CSS architecture in comments
- [ ] Add migration notes (lessons learned, gotchas)
### 13.2 Clean Up React Files
- [ ] Archive React project:
- Option A: Move to `archive/react-version/`
- Option B: Create git tag `pre-blazor-migration` and delete
- Option C: Keep in separate branch
- [ ] Remove Node.js files from root:
- Delete `src/Client/package.json`
- Delete `src/Client/package-lock.json`
- Delete `src/Client/node_modules/`
- Delete `src/Client/tsconfig*.json`
- Delete `src/Client/vite.config.ts`
- Delete `src/Client/tailwind.config.js`
- Delete `src/Client/postcss.config.js`
- Delete `src/Client/.eslintrc.cjs`
- [ ] Update `.gitignore` (remove Node.js entries)
### 13.3 Code Quality
- [ ] Run `dotnet format` on all files
- [ ] Add XML documentation comments to public components
- [ ] Ensure consistent naming conventions
- [ ] Remove unused using statements
- [ ] Remove commented-out code
---
## Phase 14: Production Deployment
### 14.1 Pre-Deployment Checklist
- [ ] All components migrated and tested ✓
- [ ] All routes working ✓
- [ ] Mobile menu functional ✓
- [ ] Tabs functional ✓
- [ ] Analytics configured ✓
- [ ] SEO tags verified ✓
- [ ] Performance acceptable ✓
- [ ] No console errors ✓
- [ ] Cross-browser tested ✓
### 14.2 Deploy to Production
- [ ] Merge `blazor-refactor` branch to `main`
- [ ] Monitor GitHub Actions workflow
- [ ] Verify deployment succeeds
- [ ] Test production site at beaufindlay.com
### 14.3 Post-Deployment
- [ ] Verify site loads correctly in production
- [ ] Test all functionality in production
- [ ] Verify analytics tracking
- [ ] Monitor for errors (first 24-48 hours)
- [ ] Check browser console for any warnings
- [ ] Verify SSL certificate
- [ ] Test social media sharing previews
---
## Component Migration Checklist
### Pages (5)
- [ ] Layout (MainLayout.razor)
- [ ] Home (Pages/Home.razor)
- [ ] Work (Pages/Work.razor)
- [ ] About (Pages/About.razor)
- [ ] Error/404 (Pages/NotFound.razor)
### Typography Components (7)
- [ ] Title.razor
- [ ] Subtitle.razor
- [ ] Text.razor
- [ ] Label.razor
- [ ] List.razor
- [ ] ListItem.razor
- [ ] AnchorLink.razor
### Form Components (3)
- [ ] Button.razor
- [ ] TextInput.razor
- [ ] TextAreaInput.razor
### Display Components (5)
- [ ] TechIcons.razor
- [ ] SocialIcons.razor
- [ ] WorkTimeline.razor
- [ ] Loading.razor
- [ ] LoadingSpinner.razor
### Feature Components (3)
- [ ] NavBar.razor (in Shared/)
- [ ] Footer.razor (in Shared/)
- [ ] ContactMe.razor
- [ ] AboutTabs.razor
### Shared/Infrastructure (2)
- [ ] Icon.razor (new - SVG icon system)
- [ ] CssHelper.cs (optional - CSS class utilities)
**Total: 25 components**
---
## CSS File Structure
```
wwwroot/
├── css/
│ ├── variables.css # CSS custom properties (design tokens)
│ ├── reset.css # Modern CSS reset
│ ├── base.css # Base typography and body styles
│ ├── layout.css # Layout utilities (flex, grid, container)
│ ├── components.css # Component-specific styles
│ ├── animations.css # Animations and transitions
│ └── app.css # Main file that imports all others
├── logo.webp
├── robots.txt
└── index.html
```
---
## Key Syntax Conversions
| React/JSX | Blazor/Razor |
|-----------|--------------|
| `className="..."` | `class="..."` |
| `{variable}` | `@variable` |
| `{condition && <Element />}` | `@if (condition) { <Element /> }` |
| `{items.map(item => ...)}` | `@foreach (var item in items) { ... }` |
| `const [state, setState] = useState()` | `private bool state;` + `StateHasChanged()` |
| `onClick={handler}` | `@onclick="Handler"` |
| `onChange={handler}` | `@onchange="Handler"` |
| `<Component prop={value} />` | `<Component Prop="@value" />` |
| Props interface | `[Parameter]` properties |
| `import Component from './Component'` | `@using` or implicit |
---
## Dependencies Eliminated
### Removed Libraries
- ❌ Tailwind CSS → ✅ Vanilla CSS
- ❌ PostCSS → ✅ None
- ❌ Autoprefixer → ✅ Modern browsers only
- ❌ React Icons → ✅ Inline SVG
- ❌ Headless UI → ✅ Native HTML + CSS
- ❌ React Router DOM → ✅ Blazor Router (built-in)
- ❌ Vite → ✅ .NET SDK
- ❌ TypeScript → ✅ C#
- ❌ ESLint → ✅ Roslyn analyzers (built-in)
- ❌ Node.js → ✅ None
### Added Technology
- ✅ Static server-side rendering (Blazor SSR mode)
- ✅ Pure CSS interactivity techniques (checkbox hack, radio buttons, :target pseudo-class)
### Build Dependencies
- Before: Node.js, npm, Vite, TypeScript compiler, Tailwind CLI
- After: .NET SDK only
### Hosting Requirements
- Before: Static hosting (Azure Static Web Apps, any CDN)
- After: ASP.NET Core server (Azure App Service, Container Apps, or any host supporting .NET)
---
## Risk Assessment
### High Risk (Requires Testing)
- **Mobile menu with CSS checkbox hack:** CSS-only implementation complexity
- *Mitigation:* Well-established pattern, test thoroughly across browsers
- **Tabs with CSS (radio buttons or :target):** CSS-only implementation complexity
- *Mitigation:* Test both approaches, choose most accessible
- **Icon system:** 11 icons need SVG paths
- *Mitigation:* Download from FontAwesome/Simple Icons, test early
- **CSS-only accessibility:** Ensuring keyboard navigation works properly
- *Mitigation:* Thorough accessibility testing with screen readers
### Medium Risk
- **CSS conversion from Tailwind:** Large effort
- *Mitigation:* Component-specific CSS, test each component
- **Server hosting:** Requires server infrastructure instead of static hosting
- *Mitigation:* Azure App Service is straightforward to configure
- **Browser compatibility for CSS tricks:** Checkbox hack and advanced selectors
- *Mitigation:* Test on all major browsers, provide fallbacks if needed
### Low Risk
- **Basic component migration:** Straightforward
- **Routing:** Blazor router is similar to React Router
- **Static assets:** Simple copy operation
---
## Success Criteria
- [ ] Zero npm/Node.js dependencies
- [ ] Zero CSS framework dependencies
- [ ] Zero JavaScript (pure CSS interactivity)
- [ ] All pages render identically to React version
- [ ] Mobile menu works smoothly (CSS checkbox hack)
- [ ] Tabs work smoothly (CSS radio buttons or :target)
- [ ] Site loads in < 2 seconds on 4G (server-rendered)
- [ ] Site works with JavaScript disabled
- [ ] No WebSocket connections present
- [ ] No SignalR scripts loaded
- [ ] No JavaScript files served
- [ ] Analytics functional (server-side or noscript fallback)
- [ ] SEO tags correct
- [ ] Mobile responsive
- [ ] Cross-browser compatible (including CSS-only features)
- [ ] Keyboard accessible (Tab/Enter/Space navigation)
- [ ] Lighthouse score: 95+ (Performance, Accessibility, SEO)
- [ ] Server resource usage minimal (stateless requests only)
---
## Estimated Timeline
- **Phase 1-2 (Setup & CSS):** 2-3 days
- **Phase 3 (Icons):** 0.5-1 day
- **Phase 4 (Core Structure):** 1-2 days
- **Phase 5-6 (Pages & Components):** 3-4 days
- **Phase 7 (Interactive - CSS only):** 1-2 days
- **Phase 8 (CSS Conversion):** 2-3 days
- **Phase 9 (SEO & Analytics):** 0.5-1 day
- **Phase 10-11 (Build & Deploy):** 1-2 days (server configuration)
- **Phase 12 (Testing):** 2-3 days (including CSS-only interactivity testing)
- **Phase 13-14 (Docs & Deploy):** 1 day
**Total: 14-22 days** (depending on CSS approach and server setup complexity)
---
## Notes
- This plan prioritizes **simplicity** and **maintainability** over feature richness
- No external dependencies means **no breaking changes** from library updates
- Vanilla CSS may take longer initially but is **easier to maintain** long-term
- Static SSR provides **instant page loads** with server-side rendering and excellent SEO
- **Zero JavaScript** - pure CSS interactivity (checkbox hack, radio buttons) - **no framework hydration, no parsing overhead**
- The site will be **future-proof** with minimal maintenance requirements
- Focus on **modern CSS features** (Grid, Flexbox, Custom Properties) rather than utility classes
- Test frequently throughout migration - **don't wait until the end**
- **Tradeoff:** Requires server hosting instead of static hosting, but provides better initial load performance
- **Key Benefits:**
- No WebSocket connections = lower server resource usage, simpler architecture, no reconnection issues
- No JavaScript = works with JS disabled, faster TTI, smaller payload, privacy-friendly
- Accessible by default (native HTML form controls)

5
my-portfolio.slnx Normal file
View File

@@ -0,0 +1,5 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/BlazorApp/BlazorApp.csproj" />
</Folder>
</Solution>

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["BlazorApp.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>

View File

@@ -0,0 +1,98 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
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;
}

View File

@@ -0,0 +1,24 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorApp</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: 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") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@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;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

View File

@@ -0,0 +1,64 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,11 @@
@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 BlazorApp
@using BlazorApp.Components
@using BlazorApp.Components.Layout

25
src/BlazorApp/Program.cs Normal file
View File

@@ -0,0 +1,25 @@
using BlazorApp.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>();
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5064",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7162;http://localhost:5064",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,60 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 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-color: #212529;
--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-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--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, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
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 {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
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;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 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-color: #212529;
--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-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--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, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
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-right: 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-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
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: right;
}
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: right;
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: right;
}
::-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;
}
[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;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long