diff --git a/README.md b/README.md index fd2e221cc..5084d80e2 100644 --- a/README.md +++ b/README.md @@ -243,4 +243,4 @@ Scriptlog is Open Source and Free PHP Blog Software licensed under the [MIT Lice --- -*Thank you for creating with Scriptlog.* +*Thank you for creating with Scriptlog.* \ No newline at end of file diff --git a/composer.json b/composer.json index b5cf61956..cf10879d3 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "vlucas/phpdotenv": "^5.6", "voku/anti-xss": "^4.1" }, - "require-dev": { +"require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "phpmetrics/phpmetrics": "^2.9", "phpstan/phpstan": "^1.10", diff --git a/src/admin/admin-layout.php b/src/admin/admin-layout.php index 828a32895..d857e107f 100755 --- a/src/admin/admin-layout.php +++ b/src/admin/admin-layout.php @@ -187,7 +187,54 @@ function admin_footer($stylePath, $ubench = null) $('#summernote').summernote({ height: 300, minHeight: null, - maxHeight: null, + maxHeight: null, + toolbar: [ + ['style', ['style']], + ['font', ['bold', 'italic', 'underline', 'clear']], + ['fontname', ['fontname']], + ['color', ['color']], + ['para', ['ul', 'ol', 'paragraph']], + ['height', ['height']], + ['insert', ['link', 'picture', 'video']], + ['view', ['fullscreen', 'codeview']], + ['help', ['help']] + ], + callbacks: { + onImageUpload: function(files) { + // Upload image to server + var file = files[0]; + var formData = new FormData(); + formData.append('image', file); + formData.append('csrfToken', $('#csrf-token').val()); + + // Get post_id from hidden input if available + var postId = $('#post_id').val() || null; + if (postId) { + formData.append('post_id', postId); + } + + $.ajax({ + url: '/admin/media-upload.php', + method: 'POST', + data: formData, + processData: false, + contentType: false, + xhrFields: { + withCredentials: true + }, + success: function(response) { + if (response.success && response.data && response.data.url) { + $('#summernote').summernote('insertImage', response.data.url); + } else { + alert('Failed to upload image: ' + (response.error?.message || 'Unknown error')); + } + }, + error: function(xhr, status, error) { + alert('Failed to upload image: ' + error); + } + }); + } + } }); }); @@ -225,6 +272,10 @@ function admin_footer($stylePath, $ubench = null) }); + +"> +"> + \ No newline at end of file diff --git a/src/admin/sidebar-nav.php b/src/admin/sidebar-nav.php index 7c918e750..aa33b92e0 100755 --- a/src/admin/sidebar-nav.php +++ b/src/admin/sidebar-nav.php @@ -211,7 +211,7 @@ function sidebar_navigation($module, $url, $user_id = null, $user_session = null
  • -
  • RESTful API
  • +
  • @@ -238,6 +238,7 @@ function sidebar_navigation($module, $url, $user_id = null, $user_session = null
  • +
  • diff --git a/src/admin/ui/pages/edit-page.php b/src/admin/ui/pages/edit-page.php index 31455f707..31a3781bc 100755 --- a/src/admin/ui/pages/edit-page.php +++ b/src/admin/ui/pages/edit-page.php @@ -63,7 +63,7 @@
    - +
    diff --git a/src/admin/ui/posts/edit-post.php b/src/admin/ui/posts/edit-post.php index 21db6cfb6..3cb2eecda 100755 --- a/src/admin/ui/posts/edit-post.php +++ b/src/admin/ui/posts/edit-post.php @@ -63,7 +63,7 @@
    - +
    diff --git a/src/admin/ui/setting/api-setting.php b/src/admin/ui/setting/api-setting.php index ffb4732c7..ff0dc0669 100644 --- a/src/admin/ui/setting/api-setting.php +++ b/src/admin/ui/setting/api-setting.php @@ -1,93 +1,119 @@ + + +
    + +
    +

    + Control Panel +

    + +
    + + +
    +
    +
    +
    +
    + +
    + +

    Invalid Form Data!

    + ' . safe_html($e) . '

    '; + endforeach; + ?> +
    -$api = $this->api ?? []; -$errors = $this->errors ?? []; -$status = isset($_GET['status']) ? $_GET['status'] : ""; -$csrfToken = $this->csrfToken ?? csrf_generate_token('csrfToken'); -?> + -
    - -

    Success!

    - API settings have been updated successfully. -
    - +if (isset($_GET['status']) && $_GET['status'] === 'apiConfigUpdated') : + ?> - -
    +
    -

    Error!

    - -

    - +

    Success!

    +

    API settings have been updated successfully.

    - -
    -
    -
    -
    -

    RESTful API Settings

    -
    + "> - - -
    - -
    - -

    Protect your API from abuse by limiting the number of requests per client.

    -
    +$action = (isset($formAction)) ? $formAction : null; +?> -
    -
    - -
    - - -

    Maximum GET requests per client per minute. Default: 60

    -
    -
    +
    +
    + -
    - -
    - - -

    Maximum POST/PUT/DELETE/PATCH requests per client per minute. Default: 20

    -
    -
    -
    + +
    + +

    Protect your API from abuse by limiting the number of requests per client.

    +
    - -
    -

    How Rate Limiting Works

    -

    Rate limits are tracked per client using the following priority:

    -
      -
    1. API Key (X-API-Key header)
    2. -
    3. Bearer Token (Authorization header)
    4. -
    5. IP Address (fallback)
    6. -
    -

    When a client exceeds the rate limit, they receive a 429 Too Many Requests response with a Retry-After header.

    -
    +
    +
    + +
    + + +

    Maximum GET requests per client per minute. Default: 60

    +
    - + + +
    +

    How Rate Limiting Works

    +

    Rate limits are tracked per client using the following priority:

    +
      +
    1. API Key (X-API-Key header)
    2. +
    3. Bearer Token (Authorization header)
    4. +
    5. IP Address (fallback)
    6. +
    +

    When a client exceeds the rate limit, they receive a 429 Too Many Requests response with a Retry-After header.

    +
    +
    + + + +
    + +
    + +
    +
    +
    + diff --git a/src/api/index.php b/src/api/index.php index 044cc471b..7db009994 100644 --- a/src/api/index.php +++ b/src/api/index.php @@ -64,6 +64,17 @@ // Load required core files require_once __DIR__ . '/../lib/main.php'; + +// Ensure sessions are properly started for API requests that need authentication +// This is needed for endpoints like media upload which require admin session +if (isset($app->sessionMaker)) { + session_save_path(sys_get_temp_dir()); + session_set_save_handler($app->sessionMaker, true); + register_shutdown_function('session_write_close'); + if (function_exists('start_session_on_site')) { + start_session_on_site($app->sessionMaker); + } +} require_once __DIR__ . '/../lib/core/ApiAuth.php'; require_once __DIR__ . '/../lib/core/ApiResponse.php'; require_once __DIR__ . '/../lib/core/ApiRouter.php'; @@ -77,6 +88,7 @@ require_once __DIR__ . '/../lib/controller/api/TranslationsApiController.php'; require_once __DIR__ . '/../lib/controller/api/SearchApiController.php'; require_once __DIR__ . '/../lib/controller/api/ProtectedPostApiController.php'; +require_once __DIR__ . '/../lib/controller/api/MediaApiController.php'; require_once __DIR__ . '/../lib/utility/rate-limiter.php'; require_once __DIR__ . '/../lib/core/ApiHateoas.php'; @@ -164,6 +176,9 @@ $router->post('posts/(?P[0-9]+)/unlock', 'ProtectedPostApiController@unlock'); $router->post('posts/(?P[0-9]+)/verify', 'ProtectedPostApiController@verify'); + // Media API (for SummerNote image upload) + $router->post('media/upload', 'MediaApiController@upload'); + // Categories/Topics API $router->get('categories', 'CategoriesApiController@index'); $router->get('categories/(?P[0-9]+)', 'CategoriesApiController@show'); diff --git a/src/docs/API_DOCUMENTATION.md b/src/docs/API_DOCUMENTATION.md index f232f82a0..b1b1f4605 100755 --- a/src/docs/API_DOCUMENTATION.md +++ b/src/docs/API_DOCUMENTATION.md @@ -23,7 +23,7 @@ The ScriptLog RESTful API provides programmatic access to your blog's content, allowing other platforms, operating systems, and devices to interact with your blog data. The API follows REST architectural principles and returns JSON responses. -**API Version:** 1.0.0 +**API Version:** 1.1.1 **Format:** JSON --- @@ -81,6 +81,33 @@ Authorization: Bearer your-bearer-token ## API Endpoints +### API Settings (Admin Panel) + +The API includes configurable rate limiting settings that can be managed through the admin panel. + +#### Access API Settings + +Navigate to **Settings → API** in the admin panel: + +``` +https://blogware.site/admin/index.php?load=option-api&action=apiConfig&Id=0 +``` + +#### Rate Limiting Configuration + +| Setting | Description | Default | +|---------|-------------|---------| +| Enable Rate Limiting | Toggle rate limiting on/off | Enabled | +| Read Rate Limit | Maximum GET requests per minute per client | 60 | +| Write Rate Limit | Maximum POST/PUT/DELETE/PATCH requests per minute per client | 20 | + +**Note:** Rate limiting settings are stored in the `tbl_settings` table with keys: +- `api_rate_limit_enabled` (0 or 1) +- `api_rate_limit_read` (1-1000) +- `api_rate_limit_write` (1-500) + +--- + ### API Information #### Get API Information @@ -104,7 +131,7 @@ curl -X GET http://blogware.site/api/v1/ "message": "Welcome to Blogware RESTful API", "data": { "name": "Blogware RESTful API", - "version": "1.0.0", + "version": "1.1.1", "description": "RESTful API for Blogware content management system", "base_url": "/api/v1", "authentication": { @@ -274,6 +301,16 @@ Updates an existing blog post. **Requires authentication.** --- +#### Partially Update Post + +``` +PATCH /api/v1/posts/{id} +``` + +Partially updates an existing blog post. **Requires authentication.** Only send the fields you want to change. + +--- + #### Delete Post ``` @@ -372,6 +409,16 @@ Updates a category. **Requires authentication.** --- +#### Partially Update Category + +``` +PATCH /api/v1/categories/{id} +``` + +Partially updates a category. **Requires authentication.** Only send the fields you want to change. + +--- + #### Delete Category ``` @@ -447,6 +494,16 @@ Updates a comment. **Requires authentication.** --- +#### Partially Update Comment + +``` +PATCH /api/v1/comments/{id} +``` + +Partially updates a comment. **Requires authentication.** Only send the fields you want to change. + +--- + #### Delete Comment ``` @@ -630,15 +687,229 @@ curl -X GET "http://blogware.site/api/v1/posts?sort_by=post_date&sort_order=DESC ## Rate Limiting -API requests are rate limited to ensure fair usage. Rate limit headers are included in responses: +API requests are rate limited to ensure fair usage and prevent abuse. Rate limiting is applied per-client using IP address, API key, or Bearer token as the identifier. + +### Rate Limits + +| Endpoint Type | Limit | Window | +|--------------|-------|--------| +| **Read (GET)** | 60 requests | 60 seconds | +| **Write (POST/PUT/DELETE/PATCH)** | 20 requests | 60 seconds | + +### Rate Limit Headers + +All API responses include rate limit headers: | Header | Description | |--------|-------------| -| X-RateLimit-Limit | Maximum requests per minute | +| X-RateLimit-Limit | Maximum requests allowed per window | | X-RateLimit-Remaining | Remaining requests in current window | | X-RateLimit-Reset | Unix timestamp when the rate limit resets | +| Retry-After | Seconds to wait before retrying (only on 429 responses) | + +### Rate Limit Exceeded + +If you exceed the rate limit, you'll receive a `429 Too Many Requests` response: + +```json +{ + "success": false, + "status": 429, + "error": { + "code": "RATE_LIMIT_EXCEEDED", + "message": "Rate limit exceeded. Please slow down." + } +} +``` + +### Client Identification + +Rate limits are tracked per client using the following priority: +1. **API Key** (`X-API-Key` header) - if provided +2. **Bearer Token** (`Authorization` header) - if provided +3. **IP Address** (`REMOTE_ADDR`) - fallback + +--- + +## HATEOAS (Hypermedia as the Engine of Application State) + +All API responses include HATEOAS links following [RFC 5988 (Web Linking)](https://tools.ietf.org/html/rfc5988). This allows clients to discover available actions dynamically without hardcoding URLs. + +### Response Structure -If you exceed the rate limit, you'll receive a `429 Too Many Requests` response. +Every response includes a `_links` object with discoverable navigation: + +```json +{ + "success": true, + "status": 200, + "data": { ... }, + "_links": { + "self": { + "href": "http://blogware.site/api/v1/posts/1", + "rel": "self", + "type": "GET" + }, + "collection": { + "href": "http://blogware.site/api/v1/posts", + "rel": "collection", + "type": "GET" + } + } +} +``` + +### Common Link Relations + +| Relation | Description | +|----------|-------------| +| `self` | The current resource URL | +| `collection` | The parent collection URL | +| `first` | First page of paginated results | +| `prev` | Previous page of paginated results | +| `next` | Next page of paginated results | +| `last` | Last page of paginated results | +| `canonical` | The canonical HTML URL for the resource | +| `comments` | Comments for a post | +| `post` | The parent post for a comment | +| `posts` | Posts in a category | +| `year` | Year archive for a month | +| `search` | Search endpoint (templated URL) | +| `service-desc` | OpenAPI specification URL | + +### Root API Links + +The API root (`GET /api/v1/`) returns links to all available endpoints: + +```json +{ + "success": true, + "status": 200, + "data": { + "name": "Blogware RESTful API", + "version": "1.0.0" + }, + "_links": { + "self": { + "href": "http://blogware.site/api/v1", + "rel": "self", + "type": "GET" + }, + "posts": { + "href": "http://blogware.site/api/v1/posts", + "rel": "posts", + "type": "GET" + }, + "categories": { + "href": "http://blogware.site/api/v1/categories", + "rel": "categories", + "type": "GET" + }, + "comments": { + "href": "http://blogware.site/api/v1/comments", + "rel": "comments", + "type": "GET" + }, + "archives": { + "href": "http://blogware.site/api/v1/archives", + "rel": "archives", + "type": "GET" + }, + "search": { + "href": "http://blogware.site/api/v1/search?q={query}", + "rel": "search", + "type": "GET", + "templated": true + }, + "openapi": { + "href": "http://blogware.site/api/v1/openapi.json", + "rel": "service-desc", + "type": "application/json" + } + } +} +``` + +### Paginated Response with HATEOAS + +```json +{ + "success": true, + "status": 200, + "data": [ ... ], + "pagination": { + "current_page": 2, + "per_page": 10, + "total_items": 50, + "total_pages": 5, + "has_next_page": true, + "has_previous_page": true + }, + "_links": { + "self": { + "href": "http://blogware.site/api/v1/posts?page=2&per_page=10", + "rel": "self", + "type": "GET" + }, + "first": { + "href": "http://blogware.site/api/v1/posts?page=1&per_page=10", + "rel": "first", + "type": "GET" + }, + "prev": { + "href": "http://blogware.site/api/v1/posts?page=1&per_page=10", + "rel": "prev", + "type": "GET" + }, + "next": { + "href": "http://blogware.site/api/v1/posts?page=3&per_page=10", + "rel": "next", + "type": "GET" + }, + "last": { + "href": "http://blogware.site/api/v1/posts?page=5&per_page=10", + "rel": "last", + "type": "GET" + } + } +} +``` + +### Single Resource with HATEOAS + +```json +{ + "success": true, + "status": 200, + "data": { + "id": 1, + "title": "My First Blog Post", + "slug": "my-first-blog-post" + }, + "_links": { + "self": { + "href": "http://blogware.site/api/v1/posts/1", + "rel": "self", + "type": "GET" + }, + "comments": { + "href": "http://blogware.site/api/v1/posts/1/comments", + "rel": "comments", + "type": "GET" + }, + "canonical": { + "href": "http://blogware.site/post/1/my-first-blog-post", + "rel": "canonical", + "type": "text/html" + }, + "collection": { + "href": "http://blogware.site/api/v1/posts", + "rel": "collection", + "type": "GET" + } + } +} +``` --- @@ -780,6 +1051,34 @@ For issues and questions: ## Changelog +### Version 1.1.1 (2026-04-04) +- **Caching**: GET responses are now cacheable with `Cache-Control: public, max-age=300` + - `ETag` header for entity tag cache validation + - `Last-Modified` header for timestamp-based validation + - `304 Not Modified` response for conditional requests (`If-None-Match`, `If-Modified-Since`) + - `Vary: Accept, Accept-Encoding, X-API-Key` header for proper cache keying +- **HTTP Compliance**: + - `Location` header on all `201 Created` responses + - `204 No Content` for `DELETE` operations (was `200 OK`) + - `406 Not Acceptable` for unsupported `Accept` header values + - `PATCH` method support for partial updates (Posts, Categories, Comments) + - `Allow` header on `405 Method Not Allowed` responses +- **REST Semantic Fixes**: + - Changed `POST /languages/{code}/default` to `PUT` (proper state change semantics) + +### Version 1.1.0 (2026-04-04) +- **Rate Limiting**: Implemented file-based rate limiting with sliding window + - 60 requests/minute for read operations (GET) + - 20 requests/minute for write operations (POST/PUT/DELETE/PATCH) + - Per-client tracking by API key, Bearer token, or IP address + - Standard rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) +- **HATEOAS**: Added Hypermedia as the Engine of Application State to all responses + - `_links` object in every response following RFC 5988 (Web Linking) + - Pagination links (self, first, prev, next, last) + - Resource links (self, collection, canonical, comments, post) + - Root API links for endpoint discovery + - Templated search URL support + ### Version 1.0.0 (2024-01-15) - Initial release - Posts CRUD operations diff --git a/src/docs/API_OPENAPI.json b/src/docs/API_OPENAPI.json index 84b1ce507..01f91183b 100755 --- a/src/docs/API_OPENAPI.json +++ b/src/docs/API_OPENAPI.json @@ -1,10 +1,9 @@ -Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:9003 (through xdebug.client_host/xdebug.client_port). { "openapi": "3.0.3", "info": { "title": "Blogware RESTful API", - "description": "RESTful API for Blogware content management system.\n\nThis API provides programmatic access to blog posts, categories, comments, and archives.\nIt follows OpenAPI 3.0 specification and supports both public and authenticated operations.\n\n## Authentication\n\nThe API supports two authentication methods:\n- **API Key**: Pass your API key in the `X-API-Key` header\n- **Bearer Token**: Pass a bearer token in the `Authorization` header\n\nExample:\n```\nX-API-Key: your-api-key-here\n```\nor\n```\nAuthorization: Bearer your-token-here\n```\n\n## Rate Limiting\n\nAPI requests are rate limited to ensure fair usage. The following headers are included in responses:\n- `X-RateLimit-Limit`: Maximum requests per minute\n- `X-RateLimit-Remaining`: Remaining requests in current window\n- `X-RateLimit-Reset`: Unix timestamp when the rate limit resets\n\n## Response Format\n\nAll responses are returned in JSON format with the following structure:\n\n### Success Response\n```json\n{\n \"success\": true,\n \"status\": 200,\n \"message\": \"Operation description\",\n \"data\": { ... }\n}\n```\n\n### Paginated Response\n```json\n{\n \"success\": true,\n \"status\": 200,\n \"data\": [...],\n \"pagination\": {\n \"current_page\": 1,\n \"per_page\": 10,\n \"total_items\": 50,\n \"total_pages\": 5,\n \"has_next_page\": true,\n \"has_previous_page\": false\n }\n}\n```\n\n### Error Response\n```json\n{\n \"success\": false,\n \"status\": 400,\n \"error\": {\n \"code\": \"BAD_REQUEST\",\n \"message\": \"Error description\"\n }\n}\n```\n\n## Filtering and Sorting\n\nQuery parameters for filtering and sorting:\n- `page`: Page number (default: 1)\n- `per_page`: Items per page (default: 10, max: 100)\n- `sort_by`: Field to sort by\n- `sort_order`: Sort direction (ASC or DESC)\n", - "version": "1.0.0", + "description": "RESTful API for Blogware content management system.\n\nThis API provides programmatic access to blog posts, categories, comments, and archives.\nIt follows OpenAPI 3.0 specification and supports both public and authenticated operations.\n\n## Authentication\n\nThe API supports two authentication methods:\n- **API Key**: Pass your API key in the `X-API-Key` header\n- **Bearer Token**: Pass a bearer token in the `Authorization` header\n\nExample:\n```\nX-API-Key: your-api-key-here\n```\nor\n```\nAuthorization: Bearer your-token-here\n```\n\n## Rate Limiting\n\nAPI requests are rate limited to ensure fair usage and prevent abuse:\n\n| Endpoint Type | Limit | Window |\n|--------------|-------|--------|\n| **Read (GET)** | 60 requests | 60 seconds |\n| **Write (POST/PUT/DELETE/PATCH)** | 20 requests | 60 seconds |\n\nRate limits are tracked per client using API key, Bearer token, or IP address.\n\nThe following headers are included in all responses:\n- `X-RateLimit-Limit`: Maximum requests allowed per window\n- `X-RateLimit-Remaining`: Remaining requests in current window\n- `X-RateLimit-Reset`: Unix timestamp when the rate limit resets\n- `Retry-After`: Seconds to wait before retrying (only on 429 responses)\n\nExceeding the rate limit returns `429 Too Many Requests`.\n\n## HATEOAS (Hypermedia as the Engine of Application State)\n\nAll API responses include HATEOAS links following [RFC 5988 (Web Linking)](https://tools.ietf.org/html/rfc5988).\nEach response contains a `_links` object with discoverable navigation:\n\n```json\n{\n \"success\": true,\n \"status\": 200,\n \"data\": { ... },\n \"_links\": {\n \"self\": { \"href\": \"...\", \"rel\": \"self\", \"type\": \"GET\" },\n \"collection\": { \"href\": \"...\", \"rel\": \"collection\", \"type\": \"GET\" }\n }\n}\n```\n\nCommon link relations: `self`, `collection`, `first`, `prev`, `next`, `last`, `canonical`, `comments`, `post`, `posts`, `year`, `search`, `service-desc`.\n\n## Caching and Conditional Requests\n\nGET responses are cacheable with ETag and Last-Modified headers:\n- **ETag**: Entity tag for cache validation\n- **Last-Modified**: Timestamp of last resource modification\n- **If-None-Match**: Client sends cached ETag to check for changes\n- **If-Modified-Since**: Client sends cached timestamp\n- **304 Not Modified**: Response when resource hasn't changed\n\nCache-Control headers vary by HTTP method:\n- **GET**: `public, max-age=300` (cacheable for 5 minutes)\n- **POST/PUT/DELETE/PATCH**: `no-store` (never cache)\n- **4xx errors**: `no-cache, no-store, must-revalidate`\n\n## Content Negotiation\n\nThe API supports `Accept` header validation. Requests with unsupported media types receive `406 Not Acceptable`. Currently only `application/json` is supported.\n\n## Response Format\n\nAll responses are returned in JSON format with the following structure:\n\n### Success Response\n```json\n{\n \"success\": true,\n \"status\": 200,\n \"message\": \"Operation description\",\n \"data\": { ... }\n}\n```\n\n### Paginated Response\n```json\n{\n \"success\": true,\n \"status\": 200,\n \"data\": [...],\n \"pagination\": {\n \"current_page\": 1,\n \"per_page\": 10,\n \"total_items\": 50,\n \"total_pages\": 5,\n \"has_next_page\": true,\n \"has_previous_page\": false\n }\n}\n```\n\n### Error Response\n```json\n{\n \"success\": false,\n \"status\": 400,\n \"error\": {\n \"code\": \"BAD_REQUEST\",\n \"message\": \"Error description\"\n }\n}\n```\n\n## Filtering and Sorting\n\nQuery parameters for filtering and sorting:\n- `page`: Page number (default: 1)\n- `per_page`: Items per page (default: 10, max: 100)\n- `sort_by`: Field to sort by\n- `sort_order`: Sort direction (ASC or DESC)\n", + "version": "1.1.1", "contact": { "name": "Blogware Support", "email": "alanmoehammad@gmail.com" @@ -68,6 +67,9 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 } } } + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" } } } @@ -107,6 +109,9 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 }, "500": { "$ref": "#/components/responses/InternalServerError" + }, + "429": { + "$ref": "#/components/responses/TooManyRequests" } } }, @@ -277,13 +282,64 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 "$ref": "#/components/parameters/PostId" } ], + "responses": { + "204": { + "description": "Post deleted successfully (no content)" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "patch": { + "tags": [ + "Posts" + ], + "summary": "Partially update a post", + "description": "Partially updates an existing blog post. Only send the fields you want to change.\nRequires authentication with administrator or editor permissions.\n", + "operationId": "patchPost", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/PostId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostUpdate" + } + } + } + }, "responses": { "200": { - "description": "Post deleted successfully", + "description": "Post updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteResponse" + "$ref": "#/components/schemas/PostResponse" } } } @@ -553,13 +609,64 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 "$ref": "#/components/parameters/CategoryId" } ], + "responses": { + "204": { + "description": "Category deleted successfully (no content)" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "patch": { + "tags": [ + "Categories" + ], + "summary": "Partially update a category", + "description": "Partially updates an existing category/topic. Only send the fields you want to change.\nRequires authentication with administrator or editor permissions.\n", + "operationId": "patchCategory", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/CategoryId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CategoryUpdate" + } + } + } + }, "responses": { "200": { - "description": "Category deleted successfully", + "description": "Category updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteResponse" + "$ref": "#/components/schemas/CategoryResponse" } } } @@ -834,13 +941,64 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 "$ref": "#/components/parameters/CommentId" } ], + "responses": { + "204": { + "description": "Comment deleted successfully (no content)" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + }, + "patch": { + "tags": [ + "Comments" + ], + "summary": "Partially update a comment", + "description": "Partially updates an existing comment. Only send the fields you want to change.\nRequires authentication with administrator or editor permissions.\n", + "operationId": "patchComment", + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/CommentId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommentUpdate" + } + } + } + }, "responses": { "200": { - "description": "Comment deleted successfully", + "description": "Comment updated successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteResponse" + "$ref": "#/components/schemas/CommentResponse" } } } @@ -1177,6 +1335,9 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 }, "data": { "type": "object" + }, + "_links": { + "$ref": "#/components/schemas/Links" } } }, @@ -1909,6 +2070,60 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 } ] }, + "Link": { + "type": "object", + "description": "HATEOAS link following RFC 5988 (Web Linking)", + "properties": { + "href": { + "type": "string", + "description": "The URL of the linked resource", + "example": "http://blogware.site/api/v1/posts" + }, + "rel": { + "type": "string", + "description": "The relationship type", + "example": "self" + }, + "type": { + "type": "string", + "description": "The HTTP method or MIME type", + "example": "GET" + }, + "templated": { + "type": "boolean", + "description": "Whether the URL is a URI template", + "example": false + } + }, + "required": [ + "href", + "rel" + ] + }, + "Links": { + "type": "object", + "description": "HATEOAS links for discoverable API navigation", + "additionalProperties": { + "$ref": "#/components/schemas/Link" + }, + "example": { + "self": { + "href": "http://blogware.site/api/v1/posts?page=1", + "rel": "self", + "type": "GET" + }, + "next": { + "href": "http://blogware.site/api/v1/posts?page=2", + "rel": "next", + "type": "GET" + }, + "collection": { + "href": "http://blogware.site/api/v1/posts", + "rel": "collection", + "type": "GET" + } + } + }, "Pagination": { "type": "object", "properties": { @@ -1971,7 +2186,7 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 }, "version": { "type": "string", - "example": "1.0.0" + "example": "1.1.1" }, "description": { "type": "string" @@ -2071,6 +2286,70 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 } } } + }, + "TooManyRequests": { + "description": "Too Many Requests - Rate limit exceeded", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying", + "schema": { + "type": "integer", + "example": 60 + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "NotAcceptable": { + "description": "Not Acceptable - Requested media type not supported", + "headers": { + "Accept": { + "description": "Supported media types", + "schema": { + "type": "string", + "example": "application/json" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "NotModified": { + "description": "Not Modified - Resource has not changed (use cached version)", + "headers": { + "ETag": { + "description": "Entity tag for the resource", + "schema": { + "type": "string", + "example": "\"abc123\"" + } + }, + "Last-Modified": { + "description": "Last modification timestamp", + "schema": { + "type": "string", + "format": "date-time" + } + }, + "Cache-Control": { + "description": "Cache directives", + "schema": { + "type": "string", + "example": "public, max-age=300" + } + } + } } } }, @@ -2095,4 +2374,4 @@ Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:900 ] } ] -} \ No newline at end of file +} diff --git a/src/docs/API_OPENAPI.yaml b/src/docs/API_OPENAPI.yaml index e05f134b9..580c63a05 100755 --- a/src/docs/API_OPENAPI.yaml +++ b/src/docs/API_OPENAPI.yaml @@ -24,11 +24,60 @@ info: ## Rate Limiting - API requests are rate limited to ensure fair usage. The following headers are included in responses: - - `X-RateLimit-Limit`: Maximum requests per minute + API requests are rate limited to ensure fair usage and prevent abuse: + + | Endpoint Type | Limit | Window | + |--------------|-------|--------| + | **Read (GET)** | 60 requests | 60 seconds | + | **Write (POST/PUT/DELETE/PATCH)** | 20 requests | 60 seconds | + + Rate limits are tracked per client using API key, Bearer token, or IP address. + + The following headers are included in all responses: + - `X-RateLimit-Limit`: Maximum requests allowed per window - `X-RateLimit-Remaining`: Remaining requests in current window - `X-RateLimit-Reset`: Unix timestamp when the rate limit resets + - `Retry-After`: Seconds to wait before retrying (only on 429 responses) + + Exceeding the rate limit returns `429 Too Many Requests`. + + ## HATEOAS (Hypermedia as the Engine of Application State) + All API responses include HATEOAS links following [RFC 5988 (Web Linking)](https://tools.ietf.org/html/rfc5988). + Each response contains a `_links` object with discoverable navigation: + + ```json + { + "success": true, + "status": 200, + "data": { ... }, + "_links": { + "self": { "href": "...", "rel": "self", "type": "GET" }, + "collection": { "href": "...", "rel": "collection", "type": "GET" } + } + } + ``` + + Common link relations: `self`, `collection`, `first`, `prev`, `next`, `last`, `canonical`, `comments`, `post`, `posts`, `year`, `search`, `service-desc`. + + ## Caching and Conditional Requests + + GET responses are cacheable with ETag and Last-Modified headers: + - **ETag**: Entity tag for cache validation + - **Last-Modified**: Timestamp of last resource modification + - **If-None-Match**: Client sends cached ETag to check for changes + - **If-Modified-Since**: Client sends cached timestamp + - **304 Not Modified**: Response when resource hasn't changed + + Cache-Control headers vary by HTTP method: + - **GET**: `public, max-age=300` (cacheable for 5 minutes) + - **POST/PUT/DELETE/PATCH**: `no-store` (never cache) + - **4xx errors**: `no-cache, no-store, must-revalidate` + + ## Content Negotiation + + The API supports `Accept` header validation. Requests with unsupported media types receive `406 Not Acceptable`. Currently only `application/json` is supported. + ## Response Format All responses are returned in JSON format with the following structure: @@ -79,7 +128,7 @@ info: - `per_page`: Items per page (default: 10, max: 100) - `sort_by`: Field to sort by - `sort_order`: Sort direction (ASC or DESC) - version: 1.0.0 + version: 1.1.1 contact: name: Blogware Support email: alanmoehammad@gmail.com @@ -122,6 +171,8 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiInfo' + '429': + $ref: '#/components/responses/TooManyRequests' /posts: get: @@ -146,6 +197,8 @@ paths: $ref: '#/components/schemas/PostListResponse' '500': $ref: '#/components/responses/InternalServerError' + '429': + $ref: '#/components/responses/TooManyRequests' post: tags: - Posts @@ -254,13 +307,45 @@ paths: - BearerAuth: [] parameters: - $ref: '#/components/parameters/PostId' + responses: + '204': + description: Post deleted successfully (no content) + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + patch: + tags: + - Posts + summary: Partially update a post + description: | + Partially updates an existing blog post. Only send the fields you want to change. + Requires authentication with administrator or editor permissions. + operationId: patchPost + security: + - ApiKeyAuth: [] + - BearerAuth: [] + parameters: + - $ref: '#/components/parameters/PostId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PostUpdate' responses: '200': - description: Post deleted successfully + description: Post updated successfully content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/PostResponse' '400': $ref: '#/components/responses/BadRequest' '401': @@ -430,13 +515,45 @@ paths: - BearerAuth: [] parameters: - $ref: '#/components/parameters/CategoryId' + responses: + '204': + description: Category deleted successfully (no content) + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + patch: + tags: + - Categories + summary: Partially update a category + description: | + Partially updates an existing category/topic. Only send the fields you want to change. + Requires authentication with administrator or editor permissions. + operationId: patchCategory + security: + - ApiKeyAuth: [] + - BearerAuth: [] + parameters: + - $ref: '#/components/parameters/CategoryId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CategoryUpdate' responses: '200': - description: Category deleted successfully + description: Category updated successfully content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/CategoryResponse' '400': $ref: '#/components/responses/BadRequest' '401': @@ -611,13 +728,45 @@ paths: - BearerAuth: [] parameters: - $ref: '#/components/parameters/CommentId' + responses: + '204': + description: Comment deleted successfully (no content) + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + patch: + tags: + - Comments + summary: Partially update a comment + description: | + Partially updates an existing comment. Only send the fields you want to change. + Requires authentication with administrator or editor permissions. + operationId: patchComment + security: + - ApiKeyAuth: [] + - BearerAuth: [] + parameters: + - $ref: '#/components/parameters/CommentId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CommentUpdate' responses: '200': - description: Comment deleted successfully + description: Comment updated successfully content: application/json: schema: - $ref: '#/components/schemas/DeleteResponse' + $ref: '#/components/schemas/CommentResponse' '400': $ref: '#/components/responses/BadRequest' '401': @@ -863,6 +1012,8 @@ components: example: Operation successful data: type: object + _links: + $ref: '#/components/schemas/Links' # Post Schemas Post: @@ -1365,6 +1516,49 @@ components: $ref: '#/components/schemas/Pagination' # Utility Schemas + Link: + type: object + description: HATEOAS link following RFC 5988 (Web Linking) + properties: + href: + type: string + description: The URL of the linked resource + example: http://blogware.site/api/v1/posts + rel: + type: string + description: The relationship type + example: self + type: + type: string + description: The HTTP method or MIME type + example: GET + templated: + type: boolean + description: Whether the URL is a URI template + example: false + required: + - href + - rel + + Links: + type: object + description: HATEOAS links for discoverable API navigation + additionalProperties: + $ref: '#/components/schemas/Link' + example: + self: + href: http://blogware.site/api/v1/posts?page=1 + rel: self + type: GET + next: + href: http://blogware.site/api/v1/posts?page=2 + rel: next + type: GET + collection: + href: http://blogware.site/api/v1/posts + rel: collection + type: GET + Pagination: type: object properties: @@ -1409,7 +1603,7 @@ components: example: Blogware RESTful API version: type: string - example: 1.0.0 + example: 1.1.1 description: type: string base_url: @@ -1468,6 +1662,48 @@ components: application/json: schema: $ref: '#/components/schemas/Error' + TooManyRequests: + description: Too Many Requests - Rate limit exceeded + headers: + Retry-After: + description: Seconds to wait before retrying + schema: + type: integer + example: 60 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotAcceptable: + description: Not Acceptable - Requested media type not supported + headers: + Accept: + description: Supported media types + schema: + type: string + example: 'application/json' + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotModified: + description: Not Modified - Resource has not changed (use cached version) + headers: + ETag: + description: Entity tag for the resource + schema: + type: string + example: '"abc123"' + Last-Modified: + description: Last modification timestamp + schema: + type: string + format: date-time + Cache-Control: + description: Cache directives + schema: + type: string + example: 'public, max-age=300' externalDocs: description: Blogware Documentation diff --git a/src/docs/I18N_ARCHITECTURE.md b/src/docs/I18N_ARCHITECTURE.md index d40aa65e7..5a538a0fe 100755 --- a/src/docs/I18N_ARCHITECTURE.md +++ b/src/docs/I18N_ARCHITECTURE.md @@ -2,8 +2,8 @@ **Project:** Blogware/Scriptlog CMS **Version:** 2.0 -**Last Updated:** March 2026 -**Status:** 🚧 Implementation In Progress +**Last Updated:** April 2026 +**Status:** ✅ Completed --- @@ -371,8 +371,8 @@ class PrivacyPolicyDao extends Dao | File | Purpose | |------|---------| -| `functions.php` | i18n helper functions (t(), locale_url(), get_locale(), is_rtl(), language_switcher()) | -| `header.php` | Dynamic lang/dir attributes, RTL CSS loading | +| `functions.php` | i18n helper functions (t(), locale_url(), get_locale(), is_rtl(), available_locales(), language_switcher()) | +| `header.php` | Dynamic lang/dir attributes, RTL CSS loading, language switcher | | `footer.php` | Copyright translation, RTL JS loading | | `sidebar.php` | Widget titles | | `single.php` | Comment form | diff --git a/src/docs/I18N_TESTING_GUIDE.md b/src/docs/I18N_TESTING_GUIDE.md index c0c3f4282..faa043100 100755 --- a/src/docs/I18N_TESTING_GUIDE.md +++ b/src/docs/I18N_TESTING_GUIDE.md @@ -30,14 +30,16 @@ This guide covers testing procedures for the i18n (internationalization) system ``` tests/ ├── unit/ -│ ├── LocaleDetectorTest.php ✅ Pass -│ ├── TranslationLoaderTest.php ✅ Pass -│ ├── I18nManagerTest.php ✅ Pass +│ ├── LocaleDetectorTest.php ✅ Pass (17 tests) +│ ├── TranslationLoaderTest.php ✅ Pass (9 tests) +│ ├── I18nManagerTest.php ✅ Pass (29 tests) │ ├── LocaleRouterTest.php ✅ Pass -│ └── ThemeI18nTest.php ✅ Pass (NEW - Frontend JSON i18n) +│ ├── ThemeI18nTest.php ✅ Pass (26 tests - Frontend JSON i18n) +│ └── LanguageSwitcherTest.php ✅ Pass (9 tests - NEW) ├── integration/ │ ├── LanguageDaoIntegrationTest.php -│ └── TranslationServiceIntegrationTest.php +│ ├── TranslationServiceIntegrationTest.php +│ └── PrivacyPolicyDaoIntegrationTest.php (NEW) └── setup_test_db.php ``` @@ -147,7 +149,8 @@ lib/vendor/bin/phpunit --filter "Locale|I18n|Translation|Theme" | I18nManagerTest.php | 29 | ✅ Pass | | LocaleRouterTest.php | (included) | ✅ Pass | | ThemeI18nTest.php | 26 | ✅ Pass | -| **Total** | **81+** | **✅ All Pass** | +| LanguageSwitcherTest.php | 9 | ✅ Pass (NEW) | +| **Total** | **90+** | **✅ All Pass** | ### Run with Coverage Report @@ -272,6 +275,26 @@ Tests for JSON-based frontend translation system (no database). | `testCookieConsentTranslationsInArabic` | ✅ | Cookie consent Arabic | | `testAllSupportedLocalesAreRecognized` | ✅ | All 7 locales recognized | +### LanguageSwitcherTest ✅ PASS (NEW) + +Tests for the frontend language switcher UI functionality. + +**Status:** All tests passing (9 tests) + +**Test Methods:** + +| Method | Status | Description | +|--------|--------|-------------| +| `testGetLanguageNameNative` | ✅ | Verify native language names (English, العربية, 中文, etc.) | +| `testGetLanguageNameEnglish` | ✅ | Verify English language names | +| `testGetLanguageNameWithUnknownLocale` | ✅ | Unknown locale returns capitalized code | +| `testGetAllLanguageNames` | ✅ | All language data structure is correct | +| `testLanguageSwitcherGeneratesCorrectUrlPattern` | ✅ | URL pattern `?switch-lang=XX` is correct | +| `testRedirectUrlEncoding` | ✅ | Redirect URL encoding handles special chars | +| `testAllLanguageCodesGenerateUrls` | ✅ | All 7 language codes generate valid URLs | +| `testArabicLocaleGeneration` | ✅ | Arabic native name contains RTL characters | +| `testChineseLocaleGeneration` | ✅ | Chinese native name contains CJK characters | + --- ## Integration Tests @@ -323,6 +346,82 @@ Tests for translation management with database. | `testImportFromArray` | Import translations | | `testImportFromArrayUpdatesExisting` | Test merge behavior | +### PrivacyPolicyDaoIntegrationTest + +Tests for privacy policy CRUD operations with database. + +**Prerequisites:** +- Test database must be set up +- `tbl_privacy_policies` table must exist (added to setup_test_db.php) + +**Test Methods:** + +| Method | Description | +|--------|-------------| +| `testCreatePolicy` | Create new privacy policy | +| `testFindById` | Find policy by ID | +| `testFindByIdReturnsNullForNonexistent` | Handle missing ID | +| `testFindByLocale` | Find policy by locale | +| `testFindByLocaleReturnsNullForNonexistent` | Handle missing locale | +| `testFindDefault` | Get default policy | +| `testFindAllPolicies` | Get all policies | +| `testUpdatePolicy` | Update policy | +| `testDeletePolicy` | Delete policy | +| `testSetDefaultPolicy` | Set policy as default | +| `testClearDefaultPolicy` | Clear default flag | +| `testFetchAll` | Fetch all policies | +| `testSetAsDefaultPolicy` | Set as default (alias) | +| `testPolicyExists` | Check policy existence | +| `testPolicyExistsReturnsFalse` | Check non-existent policy | +| `testCreatePolicyWithMinimalData` | Test minimal data creation | +| `testCreateDuplicateLocaleThrowsException` | Test UNIQUE constraint | + +### LanguageSwitcherIntegrationTest (PLANNED) + +Tests for the full frontend language switching flow. + +**Prerequisites:** +- Live site with database +- Browser with cookie support + +**Test Flow:** + +``` +1. User clicks language in dropdown + ↓ +2. Browser navigates to ?switch-lang=XX&redirect=URL + ↓ +3. lib/main.php processes switch-lang parameter + ↓ +4. Locale saved to $_SESSION['scriptlog_locale'] + ↓ +5. Locale saved to cookie 'scriptlog_locale' + ↓ +6. Redirect to original URL (without switch-lang param) + ↓ +7. Page loads with new locale + ↓ +8. LocaleDetector reads from session/cookie + ↓ +9. Content filtered by locale + ↓ +10. UI strings displayed in selected language +``` + +**Planned Test Methods:** + +| Method | Description | Status | +|--------|-------------|--------| +| `testSwitchLangParameterSetsSession` | Verify session is set | ⏳ TODO | +| `testSwitchLangParameterSetsCookie` | Verify cookie is set | ⏳ TODO | +| `testRedirectPreservesOriginalUrl` | Verify redirect URL | ⏳ TODO | +| `testLocalePersistsAcrossPages` | Session persists | ⏳ TODO | +| `testContentFilteredByLocale` | Posts filtered correctly | ⏳ TODO | +| `testMenuFilteredByLocale` | Menus filtered correctly | ⏳ TODO | +| `testTopicFilteredByLocale` | Topics filtered correctly | ⏳ TODO | +| `testRTLAppliedForArabic` | RTL layout applied | ⏳ TODO | +| `testRTLAppliedForOtherRTL` | Other RTL languages | ⏳ TODO | + --- ## API Testing diff --git a/src/docs/TESTING_GUIDE.md b/src/docs/TESTING_GUIDE.md index f8e3b87c1..fbf35c2e2 100755 --- a/src/docs/TESTING_GUIDE.md +++ b/src/docs/TESTING_GUIDE.md @@ -34,8 +34,9 @@ This project uses two complementary testing approaches: | Metric | Value | |--------|-------| -| **Total Tests** | 868 | -| **Assertions** | ~1000+ | +| **Total Tests** | 1,172 | +| **Test Files** | 73 | +| **Assertions** | ~1300+ | | **PHPUnit Version** | 9.6.34 | | **Target Coverage** | 40% | @@ -146,9 +147,10 @@ Fill gaps in utility function testing. | Phase 3: Core Classes | MEDIUM | 🔄 Pending | 65 | 305 | | Phase 4: Controllers | MEDIUM | 🔄 Pending | 34 | 339 | | Phase 5: Utilities | LOW | 🔄 Complete | 68 | 407 | +| Password Protected Posts | HIGH | ✅ Complete | 59 | 466 | **Total Completed**: 407 tests -**Current Total**: 868 tests +**Current Total**: 1,172 tests ### Recently Added Tests @@ -199,6 +201,32 @@ Fill gaps in utility function testing. - ✓ `tests/integration/PluginDaoIntegrationTest.php` - ✓ `tests/integration/ThemeDaoIntegrationTest.php` +#### Password-Protected Posts Tests (April 2026) + +**Total: 59 tests across 3 files** + +- ✓ `tests/unit/ProtectedPostTest.php` (12 tests) + - Tests for `protect_post()`, `encrypt_post()`, `decrypt_post()` + - Tests for `checking_post_password()`, `grab_post_protected()` + - Visibility validation tests (public, private, protected) + +- ✓ `tests/unit/ProtectedPostRateLimitTest.php` (20 tests) + - Rate limiting logic tests (5 attempts limit per 15 minutes) + - Old attempts expiration tests + - Separate limits per post ID and IP + - Password strength validation tests (length, uppercase, lowercase, number, special char) + - Session-based unlock storage tests + - Tests for: `is_unlock_rate_limited()`, `track_failed_unlock_attempt()`, `clear_failed_unlock_attempts()`, `get_failed_unlock_attempts()`, `check_post_password_strength()` + +- ✓ `tests/unit/PostControllerProtectedPostTest.php` (27 tests) + - Visibility validation tests (public, private, protected) + - Password validation for protected posts + - Content encryption/decryption flow + - Session handling for protected posts + - Form validation error handling + - CSRF protection tests + - Required field validation tests + #### Phase 2 - Service Layer Tests (Complete) - ✓ `tests/service/UserServiceTest.php` (18 tests) - ✓ `tests/service/PostServiceTest.php` (24 tests) diff --git a/src/env.sample.md b/src/env.sample.md index e7d7d08f6..d5941bf1b 100644 --- a/src/env.sample.md +++ b/src/env.sample.md @@ -22,7 +22,7 @@ MAIL_FROM_NAME= # --- SYSTEM --- SYSTEM_OS= -DISTRIB_NAME="Linux Mint" +DISTRIB_NAME= # --- API SECURITY --- CORS_ALLOWED_ORIGINS= \ No newline at end of file diff --git a/src/install/include/settings.php b/src/install/include/settings.php index b23054b4f..21dadc61a 100755 --- a/src/install/include/settings.php +++ b/src/install/include/settings.php @@ -16,7 +16,12 @@ define('APP_PATH', dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR); define('APP_INC', 'include'); -require_once __DIR__ . '/../../lib/vendor/autoload.php'; +$vendorAutoload = '/vendor/autoload.php'; +if (file_exists(__DIR__ . '/../../lib' . $vendorAutoload)) { + require_once __DIR__ . '/../../lib' . $vendorAutoload; +} elseif (file_exists(__DIR__ . '/../../../vendor/autoload.php')) { + require_once __DIR__ . '/../../../vendor/autoload.php'; +} include_once __DIR__ . '/../../lib/utility/is-ssl.php'; include_once __DIR__ . '/../../lib/utility/get-browser-name.php'; include_once __DIR__ . '/../../lib/utility/get-os.php'; diff --git a/src/install/include/setup.php b/src/install/include/setup.php index 2eb1a5bdd..7f77052fd 100755 --- a/src/install/include/setup.php +++ b/src/install/include/setup.php @@ -547,6 +547,8 @@ function install_i18n_data($link, $prefix = '', $default_lang = 'en') 'nav.membership' => ['Membership', 'العضوية', '会员资格', 'Adhésion', 'Членство', 'Membresía', 'Kanggotaan'], 'nav.download_settings' => ['Download Settings', 'إعدادات التنزيل', '下载设置', 'Paramètres de téléchargement', 'Настройки загрузки', 'Configuración de descarga', 'Pengaturan Unduhan'], 'nav.mail_settings' => ['Mail Settings', 'إعدادات البريد', '邮件设置', 'Paramètres de messagerie', 'Настройки почты', 'Configuración de correo', 'Pengaturan Surel'], + 'nav.api' => ['API', 'API', 'API', 'API', 'API', 'API', 'API'], + 'nav.api_settings' => ['API Settings', 'إعدادات API', 'API设置', 'Paramètres API', 'Настройки API', 'Configuración API', 'Pengaturan API'], 'nav.plugins' => ['Plugins', 'الإضافات', '插件', 'Extensions', 'Плагины', 'Complementos', 'Plugin'], 'nav.privacy' => ['Privacy', 'الخصوصية', '隐私', 'Confidentialité', 'Конфиденциальность', 'Privacidad', 'Privasi'], 'nav.privacy_settings' => ['Privacy Settings', 'إعدادات الخصوصية', '隐私设置', 'Paramètres de confidentialité', 'Настройки конфиденциальности', 'Configuración de privacidad', 'Pengaturan Privasi'], diff --git a/src/install/index.php b/src/install/index.php index 9bc4bc2e9..9c6993056 100644 --- a/src/install/index.php +++ b/src/install/index.php @@ -568,6 +568,4 @@ function goToStep(step) { install_footer($current_path); ob_end_flush(); - - - + \ No newline at end of file diff --git a/src/install/migrate-smtp.php b/src/install/migrate-smtp.php index 123c7cca1..6f08011c7 100755 --- a/src/install/migrate-smtp.php +++ b/src/install/migrate-smtp.php @@ -7,7 +7,13 @@ */ define('SCRIPTLOG', true); -require_once __DIR__ . '/../lib/vendor/autoload.php'; + +// Universal vendor autoload - works for both standard and Composer installations +if (file_exists(__DIR__ . '/../lib/vendor/autoload.php')) { + require_once __DIR__ . '/../lib/vendor/autoload.php'; +} elseif (file_exists(__DIR__ . '/../vendor/autoload.php')) { + require_once __DIR__ . '/../vendor/autoload.php'; +} $config = require __DIR__ . '/../config.php'; diff --git a/src/lib/controller/ConfigurationController.php b/src/lib/controller/ConfigurationController.php index 08612f8c0..9bdfc2759 100755 --- a/src/lib/controller/ConfigurationController.php +++ b/src/lib/controller/ConfigurationController.php @@ -986,6 +986,8 @@ public function updateApiSetting() $checkError = true; $checkStatus = false; + $this->setView('api-setting'); + $apiSettingKeys = [ 'api_rate_limit_enabled', 'api_rate_limit_read', diff --git a/src/lib/controller/MediaController.php b/src/lib/controller/MediaController.php index 62ec9d0c8..f2f03fd0b 100755 --- a/src/lib/controller/MediaController.php +++ b/src/lib/controller/MediaController.php @@ -315,6 +315,11 @@ public function update($id) $getMediaMeta = $this->mediaService->grabMediaMeta($getMedia['ID'], $getMedia['media_filename']); + if (empty($getMediaMeta)) { + $_SESSION['error'] = "mediaNotFound"; + direct_page('index.php?load=medialib&error=mediaNotFound', 404); + } + $media_properties = array( 'ID' => $getMediaMeta['ID'], diff --git a/src/lib/controller/PageController.php b/src/lib/controller/PageController.php index 5be887553..d4e3c791e 100755 --- a/src/lib/controller/PageController.php +++ b/src/lib/controller/PageController.php @@ -105,7 +105,7 @@ public function insert() 'post_locale' => isset($_POST['post_locale']) ? Sanitize::mildSanitizer($_POST['post_locale']) : "en" ]; - $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_content' => 50000]; + $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_content' => 500000]; try { if (!csrf_check_token('csrfToken', $_POST, 60 * 10)) { @@ -292,7 +292,7 @@ public function update($id) 'post_locale' => isset($_POST['post_locale']) ? Sanitize::mildSanitizer($_POST['post_locale']) : "en" ]; - $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_content' => 50000]; + $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_content' => 500000]; try { if (!csrf_check_token('csrfToken', $_POST, 60 * 10)) { diff --git a/src/lib/controller/PostController.php b/src/lib/controller/PostController.php index 52cb111c0..b1182b510 100755 --- a/src/lib/controller/PostController.php +++ b/src/lib/controller/PostController.php @@ -108,7 +108,7 @@ public function insert() 'post_locale' => isset($_POST['post_locale']) ? Sanitize::mildSanitizer($_POST['post_locale']) : "en" ]; - $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_tags' => 200, 'post_content' => 50000]; + $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_tags' => 200, 'post_content' => 500000]; $new_filename = generate_filename($file_name)['new_filename']; $file_extension = generate_filename($file_name)['file_extension']; @@ -460,7 +460,7 @@ public function update($id) 'post_locale' => isset($_POST['post_locale']) ? Sanitize::mildSanitizer($_POST['post_locale']) : "en" ]; - $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_tags' => 200, 'post_content' => 50000]; + $form_fields = ['post_title' => 200, 'post_summary' => 320, 'post_tags' => 200, 'post_content' => 500000]; $new_filename = generate_filename($file_name)['new_filename']; $file_extension = generate_filename($file_name)['file_extension']; diff --git a/src/lib/core/Authentication.php b/src/lib/core/Authentication.php index fd804e8e4..95f3b1cd4 100755 --- a/src/lib/core/Authentication.php +++ b/src/lib/core/Authentication.php @@ -122,10 +122,10 @@ class Authentication /** * Constant COOKIE_PATH - * Available in whole domain + * Available in whole domain (including /api/* for AJAX requests) * */ - public const COOKIE_PATH = APP_ADMIN; + public const COOKIE_PATH = '/'; public function __construct(UserDao $userDao, UserTokenDao $userToken, FormValidator $validator) { diff --git a/src/lib/dao/MediaDao.php b/src/lib/dao/MediaDao.php index 9381e47f2..84ff89bde 100644 --- a/src/lib/dao/MediaDao.php +++ b/src/lib/dao/MediaDao.php @@ -148,7 +148,7 @@ public function findMediaMetaValue($mediaId, $media_filename, $sanitize) $mediameta = $this->findRow([$idsanitized, $media_filename]); - return (empty($mediameta)) ?: $mediameta; + return $mediameta ?: null; } /** diff --git a/src/lib/dao/PrivacyPolicyDao.php b/src/lib/dao/PrivacyPolicyDao.php index d63339c6f..f6d24151a 100644 --- a/src/lib/dao/PrivacyPolicyDao.php +++ b/src/lib/dao/PrivacyPolicyDao.php @@ -60,6 +60,21 @@ public function setDefaultPolicy(int $id): void $this->modify($this->table('tbl_privacy_policies'), ['is_default' => 1], ['ID' => $id]); } + public function fetchAll(): array + { + return $this->findAllPolicies(); + } + + public function clearDefaultPolicy(): void + { + $this->dbc->dbQuery("UPDATE {$this->table('tbl_privacy_policies')} SET is_default = 0"); + } + + public function setAsDefaultPolicy(int $id): void + { + $this->setDefaultPolicy($id); + } + public function policyExists(string $locale): bool { $sql = "SELECT ID FROM {$this->table('tbl_privacy_policies')} WHERE locale = ?"; diff --git a/src/lib/main.php b/src/lib/main.php index a61f0ce8c..ad327eacb 100644 --- a/src/lib/main.php +++ b/src/lib/main.php @@ -59,3 +59,32 @@ start_session_on_site($app->sessionMaker); } } + +// Handle frontend language switch +if (isset($_GET['switch-lang']) && !empty($_GET['switch-lang'])) { + $langCode = preg_replace('/[^a-z]{2}/', '', strtolower($_GET['switch-lang'])); + $validLocales = ['en', 'ar', 'zh', 'fr', 'ru', 'es', 'id']; + + if (in_array($langCode, $validLocales)) { + $_SESSION['scriptlog_locale'] = $langCode; + setcookie('scriptlog_locale', $langCode, time() + (86400 * 365), '/'); + + // Redirect to remove switch-lang from URL + $redirectUrl = $_GET['redirect'] ?? '/'; + if (!empty($_GET['redirect'])) { + header("Location: " . urldecode($_GET['redirect'])); + } else { + // Remove switch-lang param and redirect to same page + $urlParts = parse_url($_SERVER['REQUEST_URI']); + $path = $urlParts['path'] ?? '/'; + $query = []; + if (isset($urlParts['query'])) { + parse_str($urlParts['query'], $query); + unset($query['switch-lang']); + } + $newQuery = !empty($query) ? '?' . http_build_query($query) : ''; + header("Location: " . $path . $newQuery); + } + exit(); + } +} diff --git a/src/lib/utility/.lts/cbsmdkkxvh9em9p9.php b/src/lib/utility/.lts/cbsmdkkxvh9em9p9.php new file mode 100644 index 000000000..4a2143a2c --- /dev/null +++ b/src/lib/utility/.lts/cbsmdkkxvh9em9p9.php @@ -0,0 +1,4 @@ + 'dashboard.php', 'posts' => 'posts.php', 'medialib' => 'medialib.php', + 'media-upload' => 'media-upload.php', 'downloads' => 'downloads.php', 'pages' => 'pages.php', 'topics' => 'topics.php', @@ -40,6 +41,7 @@ function admin_query() 'option-language' => 'option-language.php', 'plugins' => 'plugins.php', 'privacy' => 'privacy.php', + 'privacy-policy' => 'privacy-policy.php', 'languages' => 'languages.php', 'translations' => 'translations.php', 'logout' => 'logout.php', diff --git a/src/lib/utility/form-size-validation.php b/src/lib/utility/form-size-validation.php index 3ee439006..0590ac907 100755 --- a/src/lib/utility/form-size-validation.php +++ b/src/lib/utility/form-size-validation.php @@ -12,7 +12,7 @@ function form_size_validation(array $form_fields) $exceded_limit = false; foreach ($form_fields as $k => $v) { - if (!empty($_POST[$k]) && isset($_POST[$k][$v + 1])) { + if (!empty($_POST[$k]) && strlen($_POST[$k]) > $v) { $exceded_limit = true; } } diff --git a/src/lib/utility/get-table-prefix.php b/src/lib/utility/get-table-prefix.php index 06e7c8bd6..79c912c8b 100755 --- a/src/lib/utility/get-table-prefix.php +++ b/src/lib/utility/get-table-prefix.php @@ -11,6 +11,11 @@ */ function get_table_prefix() { + // Check if we're in test environment + if (defined('SCRIPTLOG_TEST_MODE') || isset($GLOBALS['__test_prefix'])) { + return ''; // No prefix in test database + } + static $prefix = null; if ($prefix === null) { diff --git a/src/lib/utility/theme-navigation.php b/src/lib/utility/theme-navigation.php index cd88990dc..fdfaeda20 100755 --- a/src/lib/utility/theme-navigation.php +++ b/src/lib/utility/theme-navigation.php @@ -1,16 +1,27 @@ dbQuery($sql, [$visibility]); + $stmt = $db->dbQuery($sql, [$visibility, $currentLocale]); if ($stmt && $stmt->rowCount() > 0) { while ($items = $stmt->fetch(PDO::FETCH_ASSOC)) { @@ -33,7 +44,14 @@ function theme_navigation($visibility) } } else { // mysqli style - use simpleQuery - $sql = str_replace('?', "'" . db_instance()->real_escape_string($visibility) . "'", $sql); + $escapedVisibility = db_instance()->real_escape_string($visibility); + $escapedLocale = db_instance()->real_escape_string($currentLocale); + $sql = "SELECT ID, menu_label, menu_link, menu_status, menu_visibility, parent_id, menu_sort, menu_locale + FROM tbl_menu + WHERE menu_status = 'Y' + AND menu_visibility = '$escapedVisibility' + AND (menu_locale = '$escapedLocale' OR menu_locale IS NULL OR menu_locale = '') + ORDER BY menu_sort ASC, menu_label"; $stmt = $db->simpleQuery($sql); if ($stmt->num_rows > 0) { diff --git a/src/public/files/cache/translations/en.json b/src/public/files/cache/translations/en.json deleted file mode 100644 index 6ff84fcfe..000000000 --- a/src/public/files/cache/translations/en.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "404.back_home": "Back to Home", - "404.message": "The page you are looking for was not found.", - "404.title": "404", - "admin.add_language": "Add Language", - "admin.all_languages": "All Languages", - "admin.delete_language": "Delete Language", - "admin.edit_language": "Edit Language", - "admin.translations": "Translations", - "button.add": "Add New", - "button.approve": "Approve", - "button.draft": "Save Draft", - "button.preview": "Preview", - "button.publish": "Publish", - "button.read_more": "Read More", - "button.restore": "Restore", - "button.spam": "Mark as Spam", - "button.subscribe": "Subscribe", - "button.trash": "Move to Trash", - "button.unlock": "Unlock", - "button.update": "Update", - "cookie_consent.banner.description": "We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking Accept All, you consent to our use of cookies. Read our Privacy Policy to learn more.", - "cookie_consent.banner.title": "We value your privacy", - "cookie_consent.buttons.accept": "Accept All", - "cookie_consent.buttons.learn_more": "Learn More", - "cookie_consent.buttons.reject": "Reject All", - "cookie_consent.privacy.link": "Privacy Policy", - "error.forbidden": "Access denied", - "error.invalid_input": "Invalid input", - "error.not_found": "Page not found", - "error.required": "This field is required", - "error.server_error": "Server error", - "error.wrong_password": "Incorrect password. Please try again.", - "footer.copyright": "All rights reserved", - "form.email.label": "Email (will not be published)", - "form.email.placeholder": "Enter email", - "form.name.label": "Name", - "form.name.placeholder": "Enter name", - "header.nav.about": "About", - "header.nav.blog": "Blog", - "header.nav.contact": "Contact", - "header.nav.home": "Home", - "header.nav.search": "Search", - "home.divider.view_more": "View More", - "home.hero.admin_panel": "Go to administrator panel", - "home.hero.discover_more": "Discover More", - "home.hero.scroll_down": "Scroll Down", - "home.intro.description": "Your entryway to a personal blog that lets you easily express, create, and share your ideas. Whether you are a creative writer, hobbyist, tech enthusiast, personal blogger, or someone who values digital independence, ScriptLog empowers you to craft your online presence.", - "home.intro.welcome": "Welcome to ScriptLog", - "home.latest_posts.title": "Latest from the blog", - "nav.add_language": "Add Language", - "nav.add_media": "Add New", - "nav.add_new": "Add New", - "nav.add_user": "Add New", - "nav.all_languages": "All Languages", - "nav.all_pages": "All Pages", - "nav.all_posts": "All Posts", - "nav.all_users": "All Users", - "nav.appearance": "Appearance", - "nav.approved_comments": "Approved Comments", - "nav.audit_logs": "Audit Logs", - "nav.backup": "Backup", - "nav.categories": "Categories", - "nav.comments": "Comments", - "nav.dashboard": "Dashboard", - "nav.data_requests": "Data Requests", - "nav.delete_language": "Delete Language", - "nav.downloads": "Downloads", - "nav.download_settings": "Download Settings", - "nav.edit_language": "Edit Language", - "nav.edit_page": "Edit Page", - "nav.edit_post": "Edit Post", - "nav.export": "Export", - "nav.general": "General", - "nav.import": "Import", - "nav.languages": "Languages", - "nav.language_config": "Language Configuration", - "nav.language_settings": "Choose your language", - "nav.library": "Library", - "nav.mail_settings": "Mail Settings", - "nav.media": "Media", - "nav.media_library": "Media Library", - "nav.membership": "Membership", - "nav.menus": "Menus", - "nav.navigation": "Navigation", - "nav.new_page": "New Page", - "nav.new_post": "New Post", - "nav.no_languages": "No languages found", - "nav.pages": "Pages", - "nav.pending_comments": "Pending Comments", - "nav.permalink": "Permalink", - "nav.plugins": "Plugins", - "nav.posts": "Posts", - "nav.privacy": "Privacy", - "nav.privacy_settings": "Privacy Settings", - "nav.reading": "Reading", - "nav.restore": "Restore", - "nav.settings": "Settings", - "nav.set_default": "Set Default", - "nav.spam_comments": "Spam", - "nav.themes": "Themes", - "nav.timezone": "Timezone", - "nav.tools": "Tools", - "nav.translations": "Translations", - "nav.trash_comments": "Trash", - "nav.users": "Users", - "nav.widgets": "Widgets", - "nav.your_profile": "Your Profile", - "page.static_page": "Static Page", - "privacy.contact_us": "Contact Us", - "privacy.data_security": "Data Security", - "privacy.how_we_use": "How We Use Your Information", - "privacy.information_we_collect": "Information We Collect", - "privacy.last_updated": "Last updated", - "privacy.page_title": "Privacy Policy", - "privacy.your_rights": "Your Rights", - "protected.post.description": "This post is password protected. Enter the password to view its content.", - "sidebar.archives.title": "Archives", - "sidebar.categories.title": "Categories", - "sidebar.latest_posts.title": "Latest Posts", - "sidebar.search.placeholder": "What are you looking for?", - "sidebar.search.title": "Search", - "sidebar.tags.title": "Tags", - "single.comment.label": "Type your comment", - "single.comment.leave_reply": "Leave a comment", - "single.comment.placeholder": "Enter your comment", - "single.comment.submit": "Submit Comment", - "status.draft": "Draft", - "status.pending": "Pending Review", - "status.private": "Private", - "status.publish": "Published", - "status.trash": "Trash", - "visibility.password": "Password Protected", - "visibility.private": "Private", - "visibility.public": "Public" -} \ No newline at end of file diff --git a/src/public/files/cache/translations/es.json b/src/public/files/cache/translations/es.json deleted file mode 100644 index 7022ed3df..000000000 --- a/src/public/files/cache/translations/es.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "header.nav.home": "Inicio", - "header.nav.blog": "Blog", - "header.nav.about": "Acerca de", - "header.nav.contact": "Contacto", - "header.nav.search": "Buscar", - "sidebar.search.title": "Buscar", - "sidebar.search.placeholder": "¿Qué estás buscando?", - "sidebar.latest_posts.title": "Últimas publicaciones", - "sidebar.categories.title": "Categorías", - "sidebar.archives.title": "Archivos", - "sidebar.tags.title": "Etiquetas", - "home.hero.discover_more": "Descubre más", - "home.hero.admin_panel": "Ir al panel de administrador", - "home.hero.scroll_down": "Desplázate hacia abajo", - "home.intro.welcome": "Bienvenido a ScriptLog", - "home.intro.description": "Tu entrada a un blog personal", - "home.latest_posts.title": "Lo último del blog", - "home.divider.view_more": "Ver más", - "single.comment.leave_reply": "Deja un comentario", - "single.comment.label": "Escribe tu comentario", - "single.comment.placeholder": "Ingresa tu comentario", - "single.comment.submit": "Enviar comentario", - "form.name.label": "Nombre", - "form.name.placeholder": "Ingresa nombre", - "form.email.label": "Email (no se publicará)", - "form.email.placeholder": "Ingresa email", - "footer.copyright": "Todos los derechos reservados", - "cookie_consent.banner.title": "Valoramos tu privacidad", - "cookie_consent.banner.description": "utiliza cookies para mejorar tu experiencia de navegación.", - "cookie_consent.buttons.accept": "Aceptar todo", - "cookie_consent.buttons.reject": "Rechazar todo", - "cookie_consent.buttons.learn_more": "Más información", - "cookie_consent.privacy.link": "Política de privacidad", - "404.title": "404", - "404.message": "La página que buscas no se encontró.", - "404.back_home": "Volver al inicio", - "privacy.page_title": "Política de privacidad", - "privacy.last_updated": "Última actualización", - "privacy.information_we_collect": "Información que recopilamos", - "privacy.how_we_use": "Cómo usamos su información", - "privacy.data_security": "Seguridad de datos", - "privacy.your_rights": "Sus derechos", - "privacy.contact_us": "Contáctenos", - "privacy.account_info": "Información de cuenta", - "privacy.user_content": "Contenido generado por el usuario", - "privacy.technical_info": "Información técnica", - "privacy.provide_services": "Para proporcionar, operar y mantener nuestros servicios.", - "privacy.customer_support": "Para gestionar su cuenta y brindarle soporte al cliente.", - "privacy.improve_security": "Para mejorar la seguridad y el rendimiento de nuestro software.", - "privacy.monitor_usage": "Para monitorear el uso y analizar tendencias para mejorar la experiencia del usuario.", - "privacy.password_hashing": "Hash de contraseña", - "privacy.data_encryption": "Cifrado de datos", - "privacy.xss_csrf_protection": "Protección XSS y CSRF", - "privacy.prepared_statements": "Declaraciones preparadas", - "privacy.access_rights": "Tiene derecho a acceder, actualizar o eliminar la información que tenemos sobre usted.", - "form.submit": "Enviar", - "form.cancel": "Cancelar", - "form.save": "Guardar", - "form.delete": "Eliminar", - "form.edit": "Editar", - "form.search": "Buscar...", - "form.email": "Email", - "form.password": "Contraseña", - "form.comment": "Comentario", - "button.read_more": "Leer más", - "button.subscribe": "Suscribirse", - "error.not_found": "Página no encontrada", - "error.server_error": "Error del servidor", - "pagination.previous": "Anterior", - "pagination.next": "Siguiente", - "category.uncategorized": "Sin categoría", - "post.by": "Por", - "post.on": "el", - "post.read_more": "Leer más", - "post.share": "Compartir" -} \ No newline at end of file diff --git a/src/public/themes/blog/assets/css/custom.css b/src/public/themes/blog/assets/css/custom.css index bd00e84b2..9802d806d 100755 --- a/src/public/themes/blog/assets/css/custom.css +++ b/src/public/themes/blog/assets/css/custom.css @@ -176,4 +176,588 @@ to { opacity: 1; } +} + +/* ============================================ + Language Switcher + Aligned with navigation menu + ============================================ */ + +.language-switcher { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.language-switcher > li { + display: inline-block; + vertical-align: middle; +} + +.language-switcher .btn-language { + display: inline-flex; + align-items: center; + gap: 6px; + height: 60px; + padding: 0 12px; + font-size: 13px; + font-weight: 700; + text-transform: uppercase; + line-height: 60px; + color: #222; + background: transparent; + border: none; + border-radius: 0; + transition: all 0.3s ease; + cursor: pointer; + white-space: nowrap; + letter-spacing: 0.5px; +} + +.language-switcher .btn-language:hover, +.language-switcher .btn-language:focus { + color: #1085e4; + background-color: transparent; + outline: none; +} + +.language-switcher .btn-language[aria-expanded="true"] { + color: #1085e4; + background-color: transparent; +} + +.language-switcher .btn-language i.fa-globe { + font-size: 14px; + color: #1085e4; + transition: transform 0.3s ease; + margin-right: 4px; +} + +/* Remove Bootstrap's dropdown caret - we use our own icon */ +.language-switcher .btn-language.dropdown-toggle::after { + display: none !important; +} + +.language-switcher .btn-language:hover i.fa-globe { + transform: rotate(15deg); +} + +.language-switcher .btn-language span.lang-text { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.3px; +} + +.language-switcher .btn-language span.lang-code { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 18px; + padding: 0 5px; + font-size: 10px; + font-weight: 700; + color: #fff; + background: #1085e4; + border-radius: 3px; + margin-left: 4px; +} + +/* Caret styles removed - using Bootstrap's dropdown-toggle::after */ + +/* Dropdown Menu - Aligned with navigation */ +.language-switcher .dropdown-menu { + position: absolute; + top: calc(100% + 1px); + right: 0; + min-width: 240px; + margin-top: 0; + padding: 8px 0; + background: #fff; + border: 1px solid #eee; + border-radius: 8px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12); + animation: langDropdownFadeIn 0.2s ease; + z-index: 1050; +} + +.language-switcher .dropdown-menu::before { + content: ''; + position: absolute; + top: -7px; + right: 20px; + width: 14px; + height: 14px; + background: #fff; + border-left: 1px solid #eee; + border-top: 1px solid #eee; + transform: rotate(45deg); +} + +.language-switcher .dropdown-menu .dropdown-item { + display: flex; + align-items: center; + padding: 10px 16px; + font-size: 13px; + color: #333; + background: transparent; + border: none; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.language-switcher .dropdown-menu .dropdown-item:hover, +.language-switcher .dropdown-menu .dropdown-item:focus { + background: #f5f8fa; + color: #1085e4; + transform: none; +} + +.language-switcher .dropdown-menu .dropdown-item.active { + background: #e8f4fc; + color: #1085e4; +} + +.language-switcher .dropdown-menu .lang-flag { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + margin-right: 12px; + font-size: 14px; +} + +.language-switcher .dropdown-menu .lang-flag i.text-success { + color: #27ae60; +} + +.language-switcher .dropdown-menu .lang-flag i.text-muted { + color: #ccc; + font-size: 12px; +} + +.language-switcher .dropdown-menu .lang-info { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.language-switcher .dropdown-menu .lang-native { + font-weight: 600; + font-size: 14px; + line-height: 1.3; + color: inherit; +} + +.language-switcher .dropdown-menu .lang-english { + font-size: 11px; + color: #888; + font-weight: 400; + line-height: 1.2; + margin-top: 2px; +} + +.language-switcher .dropdown-menu .dropdown-item:hover .lang-english, +.language-switcher .dropdown-menu .dropdown-item:focus .lang-english { + color: #1085e4; + opacity: 0.7; +} + +.language-switcher .dropdown-menu .lang-code-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 34px; + height: 20px; + padding: 0 6px; + font-size: 10px; + font-weight: 700; + color: #fff; + background: #95a5a6; + border-radius: 3px; + margin-left: 10px; + flex-shrink: 0; +} + +.language-switcher .dropdown-menu .dropdown-item.active .lang-code-badge { + background: #1085e4; +} + +/* Animation for dropdown */ +@keyframes langDropdownFadeIn { + from { + opacity: 0; + transform: translateY(-5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .language-switcher .btn-language { + height: 50px; + line-height: 50px; + padding: 0 10px; + } + + .language-switcher .btn-language span.lang-text { + display: none; + } + + .language-switcher .dropdown-menu { + top: auto; + bottom: calc(100% + 1px); + right: 0; + left: auto; + min-width: 200px; + } + + .language-switcher .dropdown-menu::before { + right: 15px; + } + + .language-switcher .dropdown-menu .dropdown-item { + padding: 12px 16px; + } +} + +/* RTL support */ +[dir="rtl"] .language-switcher .dropdown-menu { + right: auto; + left: 0; +} + +[dir="rtl"] .language-switcher .dropdown-menu::before { + right: auto; + left: 20px; +} + +[dir="rtl"] .language-switcher .dropdown-menu .lang-flag { + margin-right: 0; + margin-left: 12px; +} + +[dir="rtl"] .language-switcher .dropdown-menu .lang-code-badge { + margin-left: 0; + margin-right: 10px; +} + +[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:hover, +[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:focus { + transform: none; +} + +.language-switcher .btn-language { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + font-size: 0.85rem; + font-weight: 600; + color: #2c3e50; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 20px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + white-space: nowrap; +} + +.language-switcher .btn-language:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.5); + transform: translateY(-1px); +} + +.language-switcher .btn-language:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.4); + border-color: #3498db; +} + +.language-switcher .btn-language[aria-expanded="true"] { + background: rgba(255, 255, 255, 0.2); + border-color: #3498db; + color: #fff; +} + +.language-switcher .btn-language i.fa-globe { + font-size: 1.1rem; + color: #3498db; + transition: transform 0.3s ease; +} + +.language-switcher .btn-language:hover i.fa-globe { + transform: rotate(15deg); +} + +.language-switcher .btn-language span.lang-text { + font-size: 0.9rem; + letter-spacing: 0.3px; +} + +.language-switcher .btn-language span.lang-code { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 26px; + height: 20px; + padding: 0 6px; + font-size: 0.7rem; + font-weight: 700; + color: #fff; + background: #3498db; + border-radius: 4px; +} + +.language-switcher .btn-language i.fa-caret-down { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.7); + transition: transform 0.2s ease; + margin-left: 2px; +} + +.language-switcher .btn-language[aria-expanded="true"] i.fa-caret-down { + transform: rotate(180deg); +} + +.language-switcher .dropdown-menu { + position: absolute; + top: calc(100% + 10px); + right: 0; + min-width: 220px; + margin-top: 0; + padding: 8px 0; + background: #fff; + border: none; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 2px 10px rgba(0, 0, 0, 0.1); + animation: langDropdownFadeIn 0.25s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1050; +} + +.language-switcher .dropdown-menu::before { + content: ''; + position: absolute; + top: -6px; + right: 20px; + width: 12px; + height: 12px; + background: #fff; + transform: rotate(45deg); + box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.05); +} + +.language-switcher .dropdown-menu:focus { + outline: none; +} + +.language-switcher .dropdown-menu .dropdown-item { + display: flex; + align-items: center; + padding: 12px 16px; + font-size: 0.9rem; + color: #2c3e50; + background: transparent; + border: none; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.language-switcher .dropdown-menu .dropdown-item:hover, +.language-switcher .dropdown-menu .dropdown-item:focus { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + color: #3498db; + transform: translateX(4px); +} + +.language-switcher .dropdown-menu .dropdown-item.active { + background: linear-gradient(135deg, #e8f4fc 0%, #d4ecf9 100%); + color: #2980b9; +} + +.language-switcher .dropdown-menu .dropdown-item.active:hover, +.language-switcher .dropdown-menu .dropdown-item.active:focus { + background: linear-gradient(135deg, #d4ecf9 0%, #c5e3f5 100%); + color: #2980b9; +} + +.language-switcher .dropdown-menu .lang-flag { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + margin-right: 12px; + font-size: 0.85rem; +} + +.language-switcher .dropdown-menu .lang-flag i.text-success { + color: #27ae60; +} + +.language-switcher .dropdown-menu .lang-flag i.text-muted { + color: #bdc3c7; + font-size: 0.7rem; +} + +.language-switcher .dropdown-menu .lang-info { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.language-switcher .dropdown-menu .lang-native { + font-weight: 600; + font-size: 0.95rem; + line-height: 1.3; + color: inherit; +} + +.language-switcher .dropdown-menu .lang-english { + font-size: 0.75rem; + color: #7f8c8d; + font-weight: 400; + line-height: 1.2; +} + +.language-switcher .dropdown-menu .dropdown-item:hover .lang-english, +.language-switcher .dropdown-menu .dropdown-item:focus .lang-english { + color: rgba(52, 152, 219, 0.7); +} + +.language-switcher .dropdown-menu .dropdown-item.active .lang-english { + color: #5dade2; +} + +.language-switcher .dropdown-menu .lang-code-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 22px; + padding: 0 8px; + font-size: 0.7rem; + font-weight: 700; + color: #fff; + background: #95a5a6; + border-radius: 4px; + margin-left: 12px; + flex-shrink: 0; +} + +.language-switcher .dropdown-menu .dropdown-item.active .lang-code-badge { + background: #27ae60; +} + +.language-switcher .dropdown-menu .dropdown-item:hover .lang-code-badge, +.language-switcher .dropdown-menu .dropdown-menu .dropdown-item:focus .lang-code-badge { + background: #3498db; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .language-switcher .btn-language { + padding: 6px 12px; + font-size: 0.8rem; + } + + .language-switcher .btn-language span.lang-text { + display: none; + } + + .language-switcher .dropdown-menu { + top: auto; + bottom: calc(100% + 8px); + right: 0; + left: auto; + min-width: 200px; + } + + .language-switcher .dropdown-menu::before { + right: 15px; + } + + .language-switcher .dropdown-menu .dropdown-item { + padding: 14px 16px; + } + + .language-switcher .dropdown-menu .lang-native { + font-size: 1rem; + } + + .language-switcher .dropdown-menu .lang-english { + font-size: 0.8rem; + } +} + +/* Tablet responsive */ +@media (min-width: 769px) and (max-width: 1024px) { + .language-switcher .btn-language span.lang-text { + font-size: 0.8rem; + } +} + +/* Accessibility improvements */ +.language-switcher [role="menuitem"] { + outline: none; +} + +.language-switcher [role="menuitem"]:focus { + box-shadow: inset 0 0 0 2px #3498db; +} + +/* Animation for dropdown */ +@keyframes langDropdownFadeIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* RTL support */ +[dir="rtl"] .language-switcher .dropdown-menu { + right: auto; + left: 0; +} + +[dir="rtl"] .language-switcher .dropdown-menu::before { + right: auto; + left: 20px; +} + +[dir="rtl"] .language-switcher .dropdown-menu .lang-flag { + margin-right: 0; + margin-left: 12px; +} + +[dir="rtl"] .language-switcher .dropdown-menu .lang-code-badge { + margin-left: 0; + margin-right: 12px; +} + +[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:hover, +[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:focus { + transform: translateX(-4px); +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .language-switcher .btn-language { + border: 2px solid #000; + } + + .language-switcher .dropdown-menu { + border: 2px solid #000; + } } \ No newline at end of file diff --git a/src/public/themes/blog/assets/css/custom.min.css b/src/public/themes/blog/assets/css/custom.min.css index c64243d69..b7f86fac4 100755 --- a/src/public/themes/blog/assets/css/custom.min.css +++ b/src/public/themes/blog/assets/css/custom.min.css @@ -1 +1 @@ -.transparent{zoom:1;filter:alpha(opacity=50);opacity:0.5}.page-wrap{min-height:100vh}.widget.search .search-form{position:relative}.widget.search .search-results{position:absolute;top:100%;left:0;right:0;background:#fff;border:1px solid #ddd;border-top:none;max-height:400px;overflow-y:auto;z-index:1000;box-shadow:0 4px 6px rgba(0,0,0,0.1);display:none}.widget.search .search-results.show{display:block}.widget.search .search-result-item{padding:12px 15px;border-bottom:1px solid #eee;transition:background-color 0.2s}.widget.search .search-result-item:last-child{border-bottom:none}.widget.search .search-result-item:hover{background-color:#f8f9fa}.widget.search .search-result-item a{display:block;text-decoration:none}.widget.search .search-result-title{font-weight:600;color:#333;margin-bottom:4px;font-size:14px}.widget.search .search-result-excerpt{font-size:12px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget.search .search-result-type{font-size:10px;color:#999;text-transform:uppercase;margin-top:4px}.widget.search .search-no-results{padding:15px;text-align:center;color:#666;font-size:14px}.widget.search .search-loading{padding:15px;text-align:center;color:#666}.widget.search .search-error{color:#dc3545;font-size:12px;margin-top:8px;display:none}.widget.search .search-error.show{display:block}.widget.search .search-result-count{padding:8px 15px;background-color:#f8f9fa;font-size:12px;color:#666;border-bottom:1px solid #eee}.widget.search input[type="search"]{width:100%;padding:10px 40px 10px 15px;border:1px solid #ddd;border-radius:4px;font-size:14px;transition:border-color 0.2s}.widget.search input[type="search"]:focus{outline:none;border-color:#007bff}.widget.search button.submit{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:5px;color:#666}.widget.search button.submit:hover{color:#007bff}.password-protected-post .password-form-inline{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;align-items:center}.password-protected-post .password-form-inline .form-group{margin-bottom:0}.password-protected-post .unlock-post-error{font-size:14px;margin-top:10px}.password-protected-post .unlock-post-loading{font-size:14px;color:#666;margin-top:10px}.password-protected-post .unlock-post-loading i{margin-right:5px}.password-protected-content{animation:fadeIn 0.5s ease-in-out}@keyframes fadeIn{from{opacity:0}to{opacity:1}} \ No newline at end of file +.transparent{zoom:1;filter:alpha(opacity=50);opacity:0.5}.page-wrap{min-height:100vh}.widget.search .search-form{position:relative}.widget.search .search-results{position:absolute;top:100%;left:0;right:0;background:#fff;border:1px solid #ddd;border-top:none;max-height:400px;overflow-y:auto;z-index:1000;box-shadow:0 4px 6px rgba(0,0,0,0.1);display:none}.widget.search .search-results.show{display:block}.widget.search .search-result-item{padding:12px 15px;border-bottom:1px solid #eee;transition:background-color 0.2s}.widget.search .search-result-item:last-child{border-bottom:none}.widget.search .search-result-item:hover{background-color:#f8f9fa}.widget.search .search-result-item a{display:block;text-decoration:none}.widget.search .search-result-title{font-weight:600;color:#333;margin-bottom:4px;font-size:14px}.widget.search .search-result-excerpt{font-size:12px;color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget.search .search-result-type{font-size:10px;color:#999;text-transform:uppercase;margin-top:4px}.widget.search .search-no-results{padding:15px;text-align:center;color:#666;font-size:14px}.widget.search .search-loading{padding:15px;text-align:center;color:#666}.widget.search .search-error{color:#dc3545;font-size:12px;margin-top:8px;display:none}.widget.search .search-error.show{display:block}.widget.search .search-result-count{padding:8px 15px;background-color:#f8f9fa;font-size:12px;color:#666;border-bottom:1px solid #eee}.widget.search input[type="search"]{width:100%;padding:10px 40px 10px 15px;border:1px solid #ddd;border-radius:4px;font-size:14px;transition:border-color 0.2s}.widget.search input[type="search"]:focus{outline:none;border-color:#007bff}.widget.search button.submit{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:5px;color:#666}.widget.search button.submit:hover{color:#007bff}.password-protected-post .password-form-inline{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;align-items:center}.password-protected-post .password-form-inline .form-group{margin-bottom:0}.password-protected-post .unlock-post-error{font-size:14px;margin-top:10px}.password-protected-post .unlock-post-loading{font-size:14px;color:#666;margin-top:10px}.password-protected-post .unlock-post-loading i{margin-right:5px}.password-protected-content{animation:fadeIn 0.5s ease-in-out}@keyframes fadeIn{from{opacity:0}to{opacity:1}}.language-switcher{position:relative;display:inline-block;vertical-align:middle}.language-switcher>li{display:inline-block;vertical-align:middle}.language-switcher .btn-language{display:inline-flex;align-items:center;gap:6px;height:60px;padding:0 12px;font-size:13px;font-weight:700;text-transform:uppercase;line-height:60px;color:#222;background:transparent;border:none;border-radius:0;transition:all 0.3s ease;cursor:pointer;white-space:nowrap;letter-spacing:0.5px}.language-switcher .btn-language:hover,.language-switcher .btn-language:focus{color:#1085e4;background-color:transparent;outline:none}.language-switcher .btn-language[aria-expanded="true"]{color:#1085e4;background-color:transparent}.language-switcher .btn-language i.fa-globe{font-size:14px;color:#1085e4;transition:transform 0.3s ease;margin-right:4px}.language-switcher .btn-language.dropdown-toggle::after{display:none !important}.language-switcher .btn-language:hover i.fa-globe{transform:rotate(15deg)}.language-switcher .btn-language span.lang-text{font-size:13px;font-weight:700;letter-spacing:0.3px}.language-switcher .btn-language span.lang-code{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:18px;padding:0 5px;font-size:10px;font-weight:700;color:#fff;background:#1085e4;border-radius:3px;margin-left:4px}.language-switcher .dropdown-menu{position:absolute;top:calc(100%+1px);right:0;min-width:240px;margin-top:0;padding:8px 0;background:#fff;border:1px solid #eee;border-radius:8px;box-shadow:0 8px 25px rgba(0, 0, 0, 0.12);animation:langDropdownFadeIn 0.2s ease;z-index:1050}.language-switcher .dropdown-menu::before{content:'';position:absolute;top:-7px;right:20px;width:14px;height:14px;background:#fff;border-left:1px solid #eee;border-top:1px solid #eee;transform:rotate(45deg)}.language-switcher .dropdown-menu .dropdown-item{display:flex;align-items:center;padding:10px 16px;font-size:13px;color:#333;background:transparent;border:none;cursor:pointer;transition:all 0.2s ease;text-decoration:none}.language-switcher .dropdown-menu .dropdown-item:hover,.language-switcher .dropdown-menu .dropdown-item:focus{background:#f5f8fa;color:#1085e4;transform:none}.language-switcher .dropdown-menu .dropdown-item.active{background:#e8f4fc;color:#1085e4}.language-switcher .dropdown-menu .lang-flag{display:flex;align-items:center;justify-content:center;width:20px;margin-right:12px;font-size:14px}.language-switcher .dropdown-menu .lang-flag i.text-success{color:#27ae60}.language-switcher .dropdown-menu .lang-flag i.text-muted{color:#ccc;font-size:12px}.language-switcher .dropdown-menu .lang-info{display:flex;flex-direction:column;flex:1;min-width:0}.language-switcher .dropdown-menu .lang-native{font-weight:600;font-size:14px;line-height:1.3;color:inherit}.language-switcher .dropdown-menu .lang-english{font-size:11px;color:#888;font-weight:400;line-height:1.2;margin-top:2px}.language-switcher .dropdown-menu .dropdown-item:hover .lang-english,.language-switcher .dropdown-menu .dropdown-item:focus .lang-english{color:#1085e4;opacity:0.7}.language-switcher .dropdown-menu .lang-code-badge{display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:20px;padding:0 6px;font-size:10px;font-weight:700;color:#fff;background:#95a5a6;border-radius:3px;margin-left:10px;flex-shrink:0}.language-switcher .dropdown-menu .dropdown-item.active .lang-code-badge{background:#1085e4}@keyframes langDropdownFadeIn{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:translateY(0)}}@media (max-width:768px){.language-switcher .btn-language{height:50px;line-height:50px;padding:0 10px}.language-switcher .btn-language span.lang-text{display:none}.language-switcher .dropdown-menu{top:auto;bottom:calc(100%+1px);right:0;left:auto;min-width:200px}.language-switcher .dropdown-menu::before{right:15px}.language-switcher .dropdown-menu .dropdown-item{padding:12px 16px}}[dir="rtl"] .language-switcher .dropdown-menu{right:auto;left:0}[dir="rtl"] .language-switcher .dropdown-menu::before{right:auto;left:20px}[dir="rtl"] .language-switcher .dropdown-menu .lang-flag{margin-right:0;margin-left:12px}[dir="rtl"] .language-switcher .dropdown-menu .lang-code-badge{margin-left:0;margin-right:10px}[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:hover,[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:focus{transform:none}.language-switcher .btn-language{display:flex;align-items:center;gap:8px;padding:8px 14px;font-size:0.85rem;font-weight:600;color:#2c3e50;background:transparent;border:1px solid rgba(255, 255, 255, 0.3);border-radius:20px;transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1);cursor:pointer;white-space:nowrap}.language-switcher .btn-language:hover{background:rgba(255, 255, 255, 0.15);border-color:rgba(255, 255, 255, 0.5);transform:translateY(-1px)}.language-switcher .btn-language:focus{outline:none;box-shadow:0 0 0 3px rgba(52, 152, 219, 0.4);border-color:#3498db}.language-switcher .btn-language[aria-expanded="true"]{background:rgba(255, 255, 255, 0.2);border-color:#3498db;color:#fff}.language-switcher .btn-language i.fa-globe{font-size:1.1rem;color:#3498db;transition:transform 0.3s ease}.language-switcher .btn-language:hover i.fa-globe{transform:rotate(15deg)}.language-switcher .btn-language span.lang-text{font-size:0.9rem;letter-spacing:0.3px}.language-switcher .btn-language span.lang-code{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:20px;padding:0 6px;font-size:0.7rem;font-weight:700;color:#fff;background:#3498db;border-radius:4px}.language-switcher .btn-language i.fa-caret-down{font-size:0.75rem;color:rgba(255, 255, 255, 0.7);transition:transform 0.2s ease;margin-left:2px}.language-switcher .btn-language[aria-expanded="true"] i.fa-caret-down{transform:rotate(180deg)}.language-switcher .dropdown-menu{position:absolute;top:calc(100%+10px);right:0;min-width:220px;margin-top:0;padding:8px 0;background:#fff;border:none;border-radius:12px;box-shadow:0 10px 40px rgba(0, 0, 0, 0.15), 0 2px 10px rgba(0, 0, 0, 0.1);animation:langDropdownFadeIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);z-index:1050}.language-switcher .dropdown-menu::before{content:'';position:absolute;top:-6px;right:20px;width:12px;height:12px;background:#fff;transform:rotate(45deg);box-shadow:-2px -2px 4px rgba(0, 0, 0, 0.05)}.language-switcher .dropdown-menu:focus{outline:none}.language-switcher .dropdown-menu .dropdown-item{display:flex;align-items:center;padding:12px 16px;font-size:0.9rem;color:#2c3e50;background:transparent;border:none;cursor:pointer;transition:all 0.2s ease;text-decoration:none}.language-switcher .dropdown-menu .dropdown-item:hover,.language-switcher .dropdown-menu .dropdown-item:focus{background:linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);color:#3498db;transform:translateX(4px)}.language-switcher .dropdown-menu .dropdown-item.active{background:linear-gradient(135deg, #e8f4fc 0%, #d4ecf9 100%);color:#2980b9}.language-switcher .dropdown-menu .dropdown-item.active:hover,.language-switcher .dropdown-menu .dropdown-item.active:focus{background:linear-gradient(135deg, #d4ecf9 0%, #c5e3f5 100%);color:#2980b9}.language-switcher .dropdown-menu .lang-flag{display:flex;align-items:center;justify-content:center;width:24px;margin-right:12px;font-size:0.85rem}.language-switcher .dropdown-menu .lang-flag i.text-success{color:#27ae60}.language-switcher .dropdown-menu .lang-flag i.text-muted{color:#bdc3c7;font-size:0.7rem}.language-switcher .dropdown-menu .lang-info{display:flex;flex-direction:column;flex:1;min-width:0}.language-switcher .dropdown-menu .lang-native{font-weight:600;font-size:0.95rem;line-height:1.3;color:inherit}.language-switcher .dropdown-menu .lang-english{font-size:0.75rem;color:#7f8c8d;font-weight:400;line-height:1.2}.language-switcher .dropdown-menu .dropdown-item:hover .lang-english,.language-switcher .dropdown-menu .dropdown-item:focus .lang-english{color:rgba(52, 152, 219, 0.7)}.language-switcher .dropdown-menu .dropdown-item.active .lang-english{color:#5dade2}.language-switcher .dropdown-menu .lang-code-badge{display:inline-flex;align-items:center;justify-content:center;min-width:32px;height:22px;padding:0 8px;font-size:0.7rem;font-weight:700;color:#fff;background:#95a5a6;border-radius:4px;margin-left:12px;flex-shrink:0}.language-switcher .dropdown-menu .dropdown-item.active .lang-code-badge{background:#27ae60}.language-switcher .dropdown-menu .dropdown-item:hover .lang-code-badge,.language-switcher .dropdown-menu .dropdown-menu .dropdown-item:focus .lang-code-badge{background:#3498db}@media (max-width:768px){.language-switcher .btn-language{padding:6px 12px;font-size:0.8rem}.language-switcher .btn-language span.lang-text{display:none}.language-switcher .dropdown-menu{top:auto;bottom:calc(100%+8px);right:0;left:auto;min-width:200px}.language-switcher .dropdown-menu::before{right:15px}.language-switcher .dropdown-menu .dropdown-item{padding:14px 16px}.language-switcher .dropdown-menu .lang-native{font-size:1rem}.language-switcher .dropdown-menu .lang-english{font-size:0.8rem}}@media (min-width:769px) and (max-width:1024px){.language-switcher .btn-language span.lang-text{font-size:0.8rem}}.language-switcher [role="menuitem"]{outline:none}.language-switcher [role="menuitem"]:focus{box-shadow:inset 0 0 0 2px #3498db}@keyframes langDropdownFadeIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}[dir="rtl"] .language-switcher .dropdown-menu{right:auto;left:0}[dir="rtl"] .language-switcher .dropdown-menu::before{right:auto;left:20px}[dir="rtl"] .language-switcher .dropdown-menu .lang-flag{margin-right:0;margin-left:12px}[dir="rtl"] .language-switcher .dropdown-menu .lang-code-badge{margin-left:0;margin-right:12px}[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:hover,[dir="rtl"] .language-switcher .dropdown-menu .dropdown-item:focus{transform:translateX(-4px)}@media (prefers-contrast:high){.language-switcher .btn-language{border:2px solid #000}.language-switcher .dropdown-menu{border:2px solid #000}} \ No newline at end of file diff --git a/src/public/themes/blog/assets/css/style.sea.css b/src/public/themes/blog/assets/css/style.sea.css index c377603e4..a4d7819e9 100755 --- a/src/public/themes/blog/assets/css/style.sea.css +++ b/src/public/themes/blog/assets/css/style.sea.css @@ -1128,6 +1128,67 @@ footer.main-footer .title::after { display: none !important; } +/* Footer Navigation */ +footer.main-footer .footer-navigation { + padding: 20px 0; +} + +footer.main-footer .footer-nav { + margin: 0; + padding: 0; + list-style: none; +} + +footer.main-footer .footer-nav li { + margin: 0 15px; +} + +footer.main-footer .footer-nav-link { + color: #ccc; + font-size: 0.95em; + font-weight: 400; + text-decoration: none; + padding: 8px 0; + display: inline-block; + transition: color 0.3s ease, border-bottom 0.3s ease; + border-bottom: 2px solid transparent; +} + +footer.main-footer .footer-nav-link:hover, +footer.main-footer .footer-nav-link:focus { + color: #fff; + border-bottom-color: #379392; +} + +footer.main-footer .footer-divider { + border: none; + border-top: 1px solid #333; + margin: 30px 0; +} + +footer.main-footer .social-menu { + margin-top: 20px; +} + +footer.main-footer .social-menu a { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + background: #2a2a2a; + color: #ccc; + font-size: 16px; + transition: all 0.3s ease; +} + +footer.main-footer .social-menu a:hover { + background: #379392; + color: #fff; + transform: translateY(-3px); +} + /* Footer Media Query ------------------------------------- */ @media (max-width: 767px) { footer.main-footer div[class*="col-"] { @@ -1354,6 +1415,13 @@ main.posts-listing .post-footer { color: #555; } +.blog-post .post-body img { + max-width: 100%; + height: auto; + display: block; + margin: 20px 0; +} + .blog-post .posts-nav { margin-top: 50px; color: #777; diff --git a/src/public/themes/blog/assets/css/style.sea.min.css b/src/public/themes/blog/assets/css/style.sea.min.css index 455cf3edf..aeb3a0a29 100755 --- a/src/public/themes/blog/assets/css/style.sea.min.css +++ b/src/public/themes/blog/assets/css/style.sea.min.css @@ -1 +1 @@ -.text-bold{font-weight:700}.text-small{font-size:0.9rem}body{overflow-x:hidden}strong{font-weight:700}a,i,span{display:inline-block;text-decoration:none;-webkit-transition:all 0.3s;transition:all 0.3s}a:hover, a:focus,i:hover,i:focus,span:hover,span:focus{text-decoration:none}a i{-webkit-transition:none;transition:none}ul{margin:0;padding:0}section{padding:100px 0;overflow-x:hidden}button,input{outline:none !important;font-family:"Open Sans", sans-serif}button{cursor:pointer}main,aside{padding:50px 0 100px}h1 a{margin-left:10px;font-size:0.9em;opacity:0;-webkit-transition:all 0.3s;transition:all 0.3s;color:#555;text-decoration:none}h1:hover a{opacity:1}.page-header{padding-top:20px;padding-bottom:20px;background:#fafafa;margin-top:30px}.page-header h2{margin-bottom:0}.animsition{z-index:9999}.widget{margin-bottom:40px;padding:30px;border:1px solid #eee}.widget header{margin-bottom:20px}.category a{color:#379392;letter-spacing:0.08em;font-weight:700;text-transform:uppercase;text-decoration:none;font-size:13px}.category a::after{content:',';color:#ddd;display:inline-block;margin-right:5px}.category a:last-of-type::after{display:none}.category a:hover{color:#000}i[class*="icon-"]{-webkit-transform:translateY(3px);transform:translateY(3px)}#style-switch-button{z-index:9999 !important}.bg-red{background:#ff7676 !important;color:#fff}.bg-red:hover{color:#fff}.bg-blue{background:#85b4f2 !important;color:#fff}.bg-blue:hover{color:#fff}.bg-yellow{background:#ffc107 !important;color:#fff}.bg-yellow:hover{color:#fff}.bg-green{background:#54e69d !important;color:#fff}.bg-green:hover{color:#fff}.bg-orange{background:#ffc36d !important;color:#fff}.bg-orange:hover{color:#fff}.bg-violet{background:#796AEE !important;color:#fff}.bg-violet:hover{color:#fff}.bg-gray{background:#ced4da !important}.bg-white{background:#fff !important}.text-red{color:#ff7676}.text-red:hover{color:#ff7676}.text-yellow{color:#ffc107}.text-yellow:hover{color:#ffc107}.text-green{color:#54e69d}.text-green:hover{color:#54e69d}.text-orange{color:#ffc36d}.text-orange:hover{color:#ffc36d}.text-violet{color:#796AEE}.text-violet:hover{color:#796AEE}.text-blue{color:#85b4f2}.text-blue:hover{color:#85b4f2}.text-gray{color:#999}.text-gray:hover{color:#999}.text-white{color:#fff}.no-padding{padding:0 !important}.no-padding-bottom{padding-bottom:0 !important}.no-padding-top{padding-top:0 !important}.no-margin{margin:0 !important}.no-margin-bottom{margin-bottom:0 !important}.no-margin-top{margin-top:0 !important}.padding-small{padding:100px 0}.btn{border-radius:0 !important}.badge{font-weight:300}.badge-rounded{border-radius:50px}.heading-light{font-weight:300 !important}.heading-medium{font-weight:400 !important}.pagination-template li.page-item{margin:0 5px}.pagination-template a.page-link{width:40px;height:40px;line-height:27px;border-radius:50% !important;border:1px solid #ddd;color:#555;text-align:center}.pagination-template a.page-link:hover, .pagination-template a.page-link.active{background:#f5f5f5}.text-primary{color:#379392 !important}p.text-hero{font-size:1.2em}p.text-hero i{font-size:1.2em}p.small-text-hero{font-size:1em}h1,h2,h3,h4,h5,h6{margin-bottom:15px}.pagination-template li.page-item{margin:0 5px}.pagination-template a.page-link{width:40px;height:40px;line-height:27px;border-radius:50% !important;border:none;color:#555;text-align:center}.pagination-template a.page-link:hover, .pagination-template a.page-link.active{background:#eee}.container-fluid{width:100%}.text-big{font-size:1.4em;font-weight:300;line-height:1.8em;color:#111}a.hero-link{color:inherit !important;text-transform:uppercase;font-size:1em;text-decoration:none !important;margin-top:20px;font-weight:300}a.hero-link::after{content:'';width:100%;height:1px;display:block;background:#fff;-webkit-transition:all 0.3s;transition:all 0.3s}a.hero-link:hover{color:#379392 !important}a.hero-link:hover::after{background:#379392}div[class*="-btn"]{cursor:pointer}@media (max-width:767px){nav.navbar .search-btn{margin-left:0;padding-left:0;border-left:none}}@media (max-width:575px){nav.navbar .container{width:100%}.breadcrumb li{display:block;width:100%;text-align:center}}.navbar{background:#fff;padding-top:20px !important;padding-bottom:20px !important;z-index:9998;border-bottom:1px solid #ddd}.navbar .langs a:first-of-type,.navbar .search-btn{margin-left:20px;padding-left:20px;border-left:1px solid #ddd;height:20px;line-height:20px}.navbar .search-btn{color:#333;font-size:0.9em}.navbar .navbar-toggler{margin-top:5px}.navbar .navbar-toggler span{width:20px;height:2px;background:#222;margin-bottom:4px;display:block}.navbar .navbar-toggler span:last-of-type{margin-bottom:0}.navbar .navbar-toggler.active span{margin:0}.navbar .navbar-toggler.active span:first-of-type{-webkit-transform:rotate(45deg) translate(3px);transform:rotate(45deg) translate(3px)}.navbar .navbar-toggler.active span:nth-of-type(2){opacity:0}.navbar .navbar-toggler.active span:last-of-type{-webkit-transform:rotate(-45deg) translate(3px);transform:rotate(-45deg) translate(3px)}.navbar .langs a{font-size:0.8em;color:#aaa;font-weight:700}.navbar .langs a.active{color:#333}.navbar .langs span{width:15px;height:1px;background:#ddd;margin:0 5px}.navbar .search-area{display:none;z-index:9999}.navbar .search-area-inner{position:fixed;top:0;right:0;width:100vw;height:100vh;background:rgba(255, 255, 255, 0.99);padding:20px !important}.navbar .search-area-inner .close-btn{position:absolute;top:20px;right:20px}.navbar .search-area-inner .row{width:100%}.navbar .search-area-inner .form-group{position:relative}.navbar .search-area-inner .submit{background:none;border:none;position:absolute;right:10px;bottom:15px}.navbar .search-area-inner input{width:100%;border:none;border-bottom:1px solid #ddd;background:none;padding:10px 0;font-size:1.6em;font-weight:300;font-family:"Open Sans", sans-serif}.navbar .search-area-inner input::-moz-placeholder{font-family:"Open Sans", sans-serif;color:#555;font-weight:300;font-size:1.1em}.navbar .search-area-inner input::-webkit-input-placeholder{font-family:"Open Sans", sans-serif;color:#555;font-weight:300;font-size:1.1em}.navbar .search-area-inner input:-ms-input-placeholder{font-family:"Open Sans", sans-serif;color:#555;font-weight:300;font-size:1.1em}.navbar .navbar-nav a.nav-link{color:#333;margin:0 5px;font-weight:400;font-size:0.95em}.navbar .navbar-nav a.nav-link:hover{color:#379392}.navbar .navbar-nav a.nav-link.active{color:#379392;font-weight:bold}@media (max-width:991px){nav.navbar .navbar-header{width:100%}nav.navbar::after{width:100%}nav.navbar.active::after{width:calc(100% - 170px)}nav.navbar .search-area{font-size:0.75em}}section.hero{padding:0;color:#fff;filter:brightness(80%)}section.hero .container{padding:200px 20px;position:relative}section.hero h1{line-height:1.2em}section.hero .continue{position:absolute;bottom:30px;left:20px;text-decoration:none !important;color:inherit !important;text-transform:uppercase;font-size:0.75em;opacity:0.8}section.hero .continue i{margin-right:5px}@media (max-width:767px){section.hero .container{padding:150px 20px}}section.featured-posts .row:nth-of-type(odd) .text{background:#fafafa}section.featured-posts .row:last-of-type{margin-bottom:0}section.featured-posts p{font-weight:400;color:#777;font-size:0.95em}section.featured-posts .text-inner{padding:70px 30px;height:100%;-webkit-transition:all 0.3s;transition:all 0.3s}section.featured-posts a{color:inherit;text-decoration:none}section.featured-posts h2{line-height:1.1em;color:#333;-webkit-transition:all 0.3s;transition:all 0.3s}section.featured-posts h2:hover{color:#555}section.featured-posts .avatar{max-width:40px;min-width:40px;height:40px;overflow:hidden;border-radius:50%;margin-right:10px}section.featured-posts .title,section.featured-posts .date,section.featured-posts .comments{font-size:0.8em;font-weight:400;color:#999}section.featured-posts .title i,section.featured-posts .date i,section.featured-posts .comments i{margin-right:5px}section.featured-posts .title::after,section.featured-posts .date::after,section.featured-posts .comments::after{content:'|';display:inline-block;margin:0 7px;font-size:0.9em;color:#ccc}section.featured-posts .comments::after{display:none}section.featured-posts .image{max-height:200px;width:auto;overflow:hidden;padding:0}section.featured-posts .image img{height:100%}section.featured-posts .post-header{margin-bottom:10px}section.featured-posts .post-footer{margin-top:30px}@media (max-width:991px){section.featured-posts .image{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1;max-height:300px;min-height:auto !important}section.featured-posts .image img{width:100%;height:auto !important}section.featured-posts .text{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}}@media (max-width:575px){section.featured-posts .post-footer{font-size:0.9em}}@media (max-width:350px){section.featured-posts .text-inner{padding:15px}section.featured-posts .post-footer{font-size:0.75em}}section.divider{color:#fff}section.latest-posts header{margin-bottom:50px}.post-meta{margin:10px 0;font-size:0.8em}.post-meta .date{text-transform:uppercase;font-weight:400}.post-meta .date::after{display:none}.post p:not(.lead){font-weight:400;color:#777;font-size:0.95em}.post a{text-decoration:none}.post a:hover, .post a:focus{text-decoration:none}.post h3{line-height:1.1em;color:#222;-webkit-transition:all 0.3s;transition:all 0.3s;margin-bottom:1rem}.post h3:hover{color:#555}.post .category a{color:#379392;letter-spacing:0.05em;font-weight:700;text-transform:uppercase;text-decoration:none}.post .date{letter-spacing:0.05em;font-weight:400;text-transform:uppercase;color:#aaa}.post .avatar{max-width:40px;min-width:40px;height:40px;overflow:hidden;border-radius:50%;margin-right:10px}.post .title,.post .date,.post .comments,.post .views{font-weight:400;color:#999;text-transform:capitalize}.post .title i,.post .date i,.post .comments i,.post .views i{margin-right:5px;font-size:1.1em}.post .title::after,.post .date::after,.post .comments::after,.post .views::after{content:'|';display:inline-block;margin:0 7px;font-size:0.9em;color:#ccc}.post .post-tags{margin-top:30px}.post .post-tags .tag{padding:5px 25px;border:1px solid #ddd;margin:5px;color:#777;font-size:0.75em;text-transform:uppercase;font-weight:600;text-decoration:none;border-radius:50px}.post .post-tags .tag:hover{background:#379392;color:#fff;border-color:#379392}.post .meta-last::after{display:none}@media (max-width:767px){.post{margin-bottom:40px}.post:last-of-type{margin-bottom:0}}section.newsletter .form-group{position:relative}section.newsletter input{width:100%;height:60px;line-height:60px;padding:0 15px;border:1px solid #111;font-size:1.1em;font-family:"Open Sans", sans-serif;font-weight:300}section.newsletter .submit{height:60px;padding:0 30px;line-height:60px;background:#111;border:1px solid #111;color:#fff;position:absolute;top:0;right:0;font-family:"Open Sans", sans-serif}@media (max-width:575px){section.newsletter input{font-size:0.95em;width:100%}section.newsletter .submit{position:static;width:100%;margin-top:10px}}section.gallery .mix{padding:0;max-height:200px;overflow:hidden}section.gallery a{outline:none;width:100%;height:100%}section.gallery a img{min-height:100%;width:100%}section.gallery .item{position:relative;width:100%;height:100%}section.gallery .item:hover .overlay{opacity:1}section.gallery .item:hover i{opacity:1;-webkit-transform:none;transform:none}section.gallery .overlay{position:absolute;top:0;right:0;width:100%;height:100%;background:rgba(55, 147, 146, 0.7);color:#fff;opacity:0;-webkit-transition:all 0.5s;transition:all 0.5s}section.gallery .overlay i{-webkit-transform:translateY(20px);transform:translateY(20px);-webkit-transition:all 0.5s;transition:all 0.5s;opacity:0;font-size:2em}footer.main-footer{background:#0e0e0e;padding:100px 0 0;color:#fff}footer.main-footer .contact-details p{font-weight:300;color:#fff;margin-bottom:5px;font-size:0.95em}footer.main-footer .contact-details a{text-decoration:underline;margin-bottom:0}footer.main-footer a{color:inherit;font-weight:300;margin-bottom:7px}footer.main-footer a:hover, footer.main-footer a:focus{color:#999}footer.main-footer ul{margin-right:30px}footer.main-footer .list-unstyled a{font-size:0.95em}footer.main-footer .latest-posts .image{max-width:50px;padding:4px;border:2px solid #333;margin-right:10px}footer.main-footer .latest-posts a{font-size:0.95em;text-decoration:none}footer.main-footer .latest-posts a:hover{color:#fff}footer.main-footer .latest-posts strong{display:block}footer.main-footer .latest-posts .date{font-size:0.85em;color:#aaa}footer.main-footer .copyrights{background:#090909;margin-top:100px;padding:20px 0;font-size:0.9em}footer.main-footer .copyrights *{margin-bottom:0}footer.main-footer .social-menu{margin-top:20px}footer.main-footer .social-menu li{padding:0 5px}footer.main-footer .date::after,footer.main-footer .title::after{display:none !important}@media (max-width:767px){footer.main-footer div[class*="col-"]{margin-bottom:40px}footer.main-footer div[class*="col-"]:last-of-type{margin-bottom:0}footer.main-footer .latest-posts>a{width:100%;margin-bottom:20px}footer.main-footer .copyrights div[class*="col-"]{margin-bottom:20px;text-align:center !important}footer.main-footer .copyrights div[class*="col-"]:last-of-type{margin-bottom:0}}main.posts-listing{padding-top:50px;padding-left:0;padding-right:0}main.posts-listing .post{margin-bottom:50px}main.posts-listing .post-footer{font-size:0.8em}.widget.search .form-group{position:relative}.widget.search input{width:100%;height:40px;line-height:40px;border:none;border-bottom:1px solid #ddd;font-size:0.95em;font-family:"Open Sans", sans-serif;font-weight:400;background:none}.widget.search input::-moz-placeholder{color:#aaa;font-family:"Open Sans", sans-serif}.widget.search input::-webkit-input-placeholder{color:#aaa;font-family:"Open Sans", sans-serif}.widget.search input:-ms-input-placeholder{color:#aaa;font-family:"Open Sans", sans-serif}.widget.search .submit{height:40px;padding:0;line-height:40px;background:none;border:none;color:#555;font-size:0.9em;position:absolute;top:0;right:0}.widget.latest-posts a{display:block;color:#555;text-decoration:none}.widget.latest-posts a:hover{color:#000}.widget.latest-posts .image{min-width:60px;max-width:60px;height:60px;overflow:hidden;margin-right:20px}.widget.latest-posts .item{margin-bottom:20px}.widget.latest-posts strong{font-size:0.95em;display:block;line-height:1em}.widget.latest-posts .views,.widget.latest-posts .comments{font-size:0.8em;font-weight:400;color:#bbb;margin-top:10px}.widget.latest-posts .views i,.widget.latest-posts .comments i{margin-right:5px}.widget.latest-posts .views::after,.widget.latest-posts .comments::after{content:'|';display:inline-block;margin:0 7px;font-size:0.9em;color:#ccc}.widget.latest-posts .comments::after{display:none}.widget.categories .item{background:#fafafa;padding:10px;color:#777;font-weight:700}.widget.categories .item:nth-of-type(2n+2){background:none}.widget.categories .item a{color:inherit;font-size:0.95em}.widget.categories .item a:hover{color:#000;text-decoration:none}.widget.categories .item span{font-size:0.9em;color:#aaa}.widget.tags .tag{padding:5px 25px;border:1px solid #ddd;margin:5px 0;color:#777;font-size:0.75em;text-transform:uppercase;font-weight:600;text-decoration:none;border-radius:50px}.widget.tags .tag:hover{background:#379392;color:#fff;border-color:#379392}.blog-post{padding-left:0;padding-right:0}.blog-post .post-footer{font-size:0.8em}.blog-post .post-thumbnail img{width:100%;margin-bottom:10px}.blog-post h1{color:#444;line-height:1.1em}.blog-post h1:hover{color:#444}.blog-post .post-footer{margin-top:20px}.blog-post .post-body{margin-top:40px}.blog-post .post-body h2,.blog-post .post-body h3,.blog-post .post-body h4,.blog-post .post-body h5,.blog-post .post-body h6{color:#333}.blog-post .post-body p{margin-bottom:30px}.blog-post .post-body p:not(.lead){font-size:1em;line-height:1.7em;color:#555}.blog-post .posts-nav{margin-top:50px;color:#777;font-size:0.8em}.blog-post .posts-nav a{color:inherit;width:calc(50% - 10px);padding:10px 20px;border:1px solid #eee;margin-bottom:15px}.blog-post .posts-nav a:hover{border-color:#379392}.blog-post .posts-nav a:hover .icon{background:#379392;color:#fff;border-color:#379392}.blog-post .icon{min-width:35px;max-width:35px;height:35px;border-radius:50%;line-height:32px;border:1px solid #ddd;color:#aaa;font-size:1.5em;text-align:center;-webkit-transition:all 0.2s;transition:all 0.2s}.blog-post .icon.prev{margin-right:20px}.blog-post .icon.next{margin-left:20px}.blog-post .post-comments{margin-top:50px}.blog-post .post-comments span.no-of-comments{color:#777;font-size:0.8em;margin-left:5px;font-weight:400}.blog-post .post-comments header{margin-bottom:40px}.blog-post .post-comments .comment:last-of-type .comment-body{border-bottom:none}.blog-post .post-comments .image{margin-right:15px}.blog-post .post-comments .title::after{display:none}.blog-post .post-comments img{max-width:40px;min-width:40px;height:40px}.blog-post .post-comments strong{display:block;color:#555}.blog-post .post-comments span.date{font-size:0.8em;color:#999}.blog-post .post-comments span.date::after{display:none}.blog-post .post-comments .comment-body{margin-left:55px;margin-top:10px;margin-bottom:25px;padding-bottom:15px;border-bottom:1px solid #eee}.blog-post .post-comments p{font-size:0.95em;color:#555}.blog-post .add-comment{margin-top:50px}.blog-post .add-comment header{margin-bottom:30px}.blog-post .add-comment input,.blog-post .add-comment textarea{background:none;border:none;border-bottom:1px solid #ddd;padding:10px 0;border-radius:0;font-family:"Open Sans", sans-serif}.blog-post .add-comment input::-moz-placeholder,.blog-post .add-comment textarea::-moz-placeholder{font-weight:400;font-size:0.9em;color:#aaa;font-weight:400;font-family:"Open Sans", sans-serif}.blog-post .add-comment input::-webkit-input-placeholder,.blog-post .add-comment textarea::-webkit-input-placeholder{font-weight:400;font-size:0.9em;color:#aaa;font-weight:400;font-family:"Open Sans", sans-serif}.blog-post .add-comment input:-ms-input-placeholder,.blog-post .add-comment textarea:-ms-input-placeholder{font-weight:400;font-size:0.9em;color:#aaa;font-weight:400;font-family:"Open Sans", sans-serif}.blog-post .add-comment input:focus,.blog-post .add-comment textarea:focus{-webkit-box-shadow:none;box-shadow:none;border-bottom:1px solid #379392}.blog-post .add-comment textarea{min-height:150px}blockquote.blockquote{font-size:1.05em;line-height:1.7em;border-color:#379392;border:1px solid #eee;border-left:6px solid #eee;padding:20px;border-top-right-radius:5px;border-bottom-right-radius:5px;margin-bottom:30px}blockquote.blockquote p{margin-bottom:15px !important}@media (max-width:767px){.posts-nav a{width:100% !important}}@media (max-width:575px){.blog-post .title::after{display:none !important}.blog-post .author{margin-bottom:10px}}#style-switch-button{position:fixed;top:120px;left:0px;border-radius:0;z-index:100000}#style-switch{width:300px;padding:20px;position:fixed;top:160px;left:0;background:#fff;border:solid 1px #ced4da;z-index:100000}#style-switch h4{color:#495057}#style-switch .text-small{font-size:.8em}#style-switch select{font-size:0.85em}.navbar{padding:0.5rem 1rem}.navbar-brand{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem;margin-right:1rem;font-size:1rem;color:#333;font-weight:bold}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.25rem;line-height:1;border:1px solid transparent;border-radius:0.25rem}.navbar-light .navbar-brand{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0, 0, 0, 0.5)}.navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover{color:rgba(0, 0, 0, 0.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0, 0, 0, 0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-toggler{color:rgba(0, 0, 0, 0.5);border-color:rgba(0, 0, 0, 0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0, 0, 0, 0.5)}.navbar-dark .navbar-brand{color:white}.navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover{color:white}.navbar-dark .navbar-nav .nav-link{color:rgba(255, 255, 255, 0.5)}.navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover{color:rgba(255, 255, 255, 0.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255, 255, 255, 0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:white}.navbar-dark .navbar-toggler{color:rgba(255, 255, 255, 0.5);border-color:rgba(255, 255, 255, 0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255, 255, 255, 0.5)}.btn{font-weight:normal;border:1px solid transparent;padding:0.5rem 1rem;font-size:1rem;line-height:1.25;border-radius:0.25rem;-webkit-transition:all 0.15s ease-in-out;transition:all 0.15s ease-in-out}.btn:focus, .btn.focus{outline:0;-webkit-box-shadow:0 0 0 3px rgba(55, 147, 146, 0.25);box-shadow:0 0 0 3px rgba(55, 147, 146, 0.25)}.btn:active, .btn.active{background-image:none}.btn-primary{color:#fff;background-color:#379392;border-color:#379392}.btn-primary:hover{color:#fff;background-color:#2d7776;border-color:#296e6d}.btn-primary:focus, .btn-primary.focus{-webkit-box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5);box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5)}.btn-primary.disabled, .btn-primary:disabled{background-color:#379392;border-color:#379392}.btn-primary:active, .btn-primary.active,.show>.btn-primary.dropdown-toggle{background-color:#2d7776;background-image:none;border-color:#296e6d}.btn-secondary{color:#fff;background-color:#868e96;border-color:#868e96}.btn-secondary:hover{color:#fff;background-color:#727b84;border-color:#6c757d}.btn-secondary:focus, .btn-secondary.focus{-webkit-box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5);box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5)}.btn-secondary.disabled, .btn-secondary:disabled{background-color:#868e96;border-color:#868e96}.btn-secondary:active, .btn-secondary.active,.show>.btn-secondary.dropdown-toggle{background-color:#727b84;background-image:none;border-color:#6c757d}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success:focus, .btn-success.focus{-webkit-box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5);box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5)}.btn-success.disabled, .btn-success:disabled{background-color:#28a745;border-color:#28a745}.btn-success:active, .btn-success.active,.show>.btn-success.dropdown-toggle{background-color:#218838;background-image:none;border-color:#1e7e34}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info:focus, .btn-info.focus{-webkit-box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5);box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5)}.btn-info.disabled, .btn-info:disabled{background-color:#17a2b8;border-color:#17a2b8}.btn-info:active, .btn-info.active,.show>.btn-info.dropdown-toggle{background-color:#138496;background-image:none;border-color:#117a8b}.btn-warning{color:#111;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#111;background-color:#e0a800;border-color:#d39e00}.btn-warning:focus, .btn-warning.focus{-webkit-box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5);box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5)}.btn-warning.disabled, .btn-warning:disabled{background-color:#ffc107;border-color:#ffc107}.btn-warning:active, .btn-warning.active,.show>.btn-warning.dropdown-toggle{background-color:#e0a800;background-image:none;border-color:#d39e00}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger:focus, .btn-danger.focus{-webkit-box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5);box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5)}.btn-danger.disabled, .btn-danger:disabled{background-color:#dc3545;border-color:#dc3545}.btn-danger:active, .btn-danger.active,.show>.btn-danger.dropdown-toggle{background-color:#c82333;background-image:none;border-color:#bd2130}.btn-light{color:#111;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#111;background-color:#e2e6ea;border-color:#dae0e5}.btn-light:focus, .btn-light.focus{-webkit-box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5);box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5)}.btn-light.disabled, .btn-light:disabled{background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:active, .btn-light.active,.show>.btn-light.dropdown-toggle{background-color:#e2e6ea;background-image:none;border-color:#dae0e5}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark:focus, .btn-dark.focus{-webkit-box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5);box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5)}.btn-dark.disabled, .btn-dark:disabled{background-color:#343a40;border-color:#343a40}.btn-dark:active, .btn-dark.active,.show>.btn-dark.dropdown-toggle{background-color:#23272b;background-image:none;border-color:#1d2124}.btn-outline-primary{color:#379392;background-color:transparent;background-image:none;border-color:#379392}.btn-outline-primary:hover{color:#fff;background-color:#379392;border-color:#379392}.btn-outline-primary:focus, .btn-outline-primary.focus{-webkit-box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5);box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5)}.btn-outline-primary.disabled, .btn-outline-primary:disabled{color:#379392;background-color:transparent}.btn-outline-primary:active, .btn-outline-primary.active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#379392;border-color:#379392}.btn-outline-secondary{color:#868e96;background-color:transparent;background-image:none;border-color:#868e96}.btn-outline-secondary:hover{color:#fff;background-color:#868e96;border-color:#868e96}.btn-outline-secondary:focus, .btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5);box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5)}.btn-outline-secondary.disabled, .btn-outline-secondary:disabled{color:#868e96;background-color:transparent}.btn-outline-secondary:active, .btn-outline-secondary.active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#868e96;border-color:#868e96}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:focus, .btn-outline-success.focus{-webkit-box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5);box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5)}.btn-outline-success.disabled, .btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:active, .btn-outline-success.active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:focus, .btn-outline-info.focus{-webkit-box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5);box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5)}.btn-outline-info.disabled, .btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:active, .btn-outline-info.active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#fff;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:focus, .btn-outline-warning.focus{-webkit-box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5);box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5)}.btn-outline-warning.disabled, .btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:active, .btn-outline-warning.active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#ffc107;border-color:#ffc107}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:focus, .btn-outline-danger.focus{-webkit-box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5);box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5)}.btn-outline-danger.disabled, .btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:active, .btn-outline-danger.active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#fff;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:focus, .btn-outline-light.focus{-webkit-box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5);box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5)}.btn-outline-light.disabled, .btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:active, .btn-outline-light.active,.show>.btn-outline-light.dropdown-toggle{color:#fff;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:focus, .btn-outline-dark.focus{-webkit-box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5);box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5)}.btn-outline-dark.disabled, .btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:active, .btn-outline-dark.active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-lg{padding:0.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:0.3rem}.btn-sm{padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5;border-radius:0.2rem}body{font-family:"Open Sans", sans-serif;font-size:1rem;font-weight:normal;line-height:1.5;color:#212529;background-color:#fff}a{color:#379392;text-decoration:none}a:focus, a:hover{color:#225b5b;text-decoration:underline}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-family:inherit;font-weight:700;line-height:1.1;color:inherit}h1,.h1{font-size:2.5rem;color:#379392}h2,.h2{font-size:2rem}h3,.h3{font-size:1.75rem}h4,.h4{font-size:1.5rem}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}.lead{font-size:1.3rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.1}.display-2{font-size:5.5rem;font-weight:300;line-height:1.1}.display-3{font-size:4.5rem;font-weight:300;line-height:1.1}.display-4{font-size:3.5rem;font-weight:300;line-height:1.1}hr{border-top:1px solid rgba(0, 0, 0, 0.1)}small,.small{font-size:80%;font-weight:normal}mark,.mark{padding:0.2em;background-color:#fcf8e3}.blockquote{padding:0.5rem 1rem;margin-bottom:1rem;font-size:1.25rem;border-left:5px solid #379392}.blockquote-footer{color:#868e96}.blockquote-footer::before{content:"\2014 \00A0"}.text-primary{color:#379392 !important}a.text-primary:focus, a.text-primary:hover{color:#296e6d !important}.page-item:first-child .page-link{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{color:#fff;background-color:#379392;border-color:#379392}.page-item.disabled .page-link{color:#868e96;background-color:#fff;border-color:#ddd}.page-link{padding:0.5rem 0.75rem;line-height:1.25;color:#379392;background-color:#fff;border:1px solid #ddd}.page-link:focus, .page-link:hover{color:#225b5b;text-decoration:none;background-color:#e9ecef;border-color:#ddd}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.bg-primary{background-color:#379392 !important}a.bg-primary:focus, a.bg-primary:hover{background-color:#296e6d !important}.bg-secondary{background-color:#868e96 !important}a.bg-secondary:focus, a.bg-secondary:hover{background-color:#6c757d !important}.bg-success{background-color:#28a745 !important}a.bg-success:focus, a.bg-success:hover{background-color:#1e7e34 !important}.bg-info{background-color:#17a2b8 !important}a.bg-info:focus, a.bg-info:hover{background-color:#117a8b !important}.bg-warning{background-color:#ffc107 !important}a.bg-warning:focus, a.bg-warning:hover{background-color:#d39e00 !important}.bg-danger{background-color:#dc3545 !important}a.bg-danger:focus, a.bg-danger:hover{background-color:#bd2130 !important}.bg-light{background-color:#f8f9fa !important}a.bg-light:focus, a.bg-light:hover{background-color:#dae0e5 !important}.bg-dark{background-color:#343a40 !important}a.bg-dark:focus, a.bg-dark:hover{background-color:#1d2124 !important}.border-primary{border-color:#379392 !important}.border-secondary{border-color:#868e96 !important}.border-success{border-color:#28a745 !important}.border-info{border-color:#17a2b8 !important}.border-warning{border-color:#ffc107 !important}.border-danger{border-color:#dc3545 !important}.border-light{border-color:#f8f9fa !important}.border-dark{border-color:#343a40 !important}.text-primary{color:#379392 !important}a.text-primary:focus, a.text-primary:hover{color:#296e6d !important}.text-secondary{color:#868e96 !important}a.text-secondary:focus, a.text-secondary:hover{color:#6c757d !important}.text-success{color:#28a745 !important}a.text-success:focus, a.text-success:hover{color:#1e7e34 !important}.text-info{color:#17a2b8 !important}a.text-info:focus, a.text-info:hover{color:#117a8b !important}.text-warning{color:#ffc107 !important}a.text-warning:focus, a.text-warning:hover{color:#d39e00 !important}.text-danger{color:#dc3545 !important}a.text-danger:focus, a.text-danger:hover{color:#bd2130 !important}.text-light{color:#f8f9fa !important}a.text-light:focus, a.text-light:hover{color:#dae0e5 !important}.text-dark{color:#343a40 !important}a.text-dark:focus, a.text-dark:hover{color:#1d2124 !important} \ No newline at end of file +.text-bold{font-weight:700}.text-small{font-size:0.9rem}body{overflow-x:hidden}strong{font-weight:700}a,i,span{display:inline-block;text-decoration:none;-webkit-transition:all 0.3s;transition:all 0.3s}a:hover, a:focus,i:hover,i:focus,span:hover,span:focus{text-decoration:none}a i{-webkit-transition:none;transition:none}ul{margin:0;padding:0}section{padding:100px 0;overflow-x:hidden}button,input{outline:none !important;font-family:"Open Sans", sans-serif}button{cursor:pointer}main,aside{padding:50px 0 100px}h1 a{margin-left:10px;font-size:0.9em;opacity:0;-webkit-transition:all 0.3s;transition:all 0.3s;color:#555;text-decoration:none}h1:hover a{opacity:1}.page-header{padding-top:20px;padding-bottom:20px;background:#fafafa;margin-top:30px}.page-header h2{margin-bottom:0}.animsition{z-index:9999}.widget{margin-bottom:40px;padding:30px;border:1px solid #eee}.widget header{margin-bottom:20px}.category a{color:#379392;letter-spacing:0.08em;font-weight:700;text-transform:uppercase;text-decoration:none;font-size:13px}.category a::after{content:',';color:#ddd;display:inline-block;margin-right:5px}.category a:last-of-type::after{display:none}.category a:hover{color:#000}i[class*="icon-"]{-webkit-transform:translateY(3px);transform:translateY(3px)}#style-switch-button{z-index:9999 !important}.bg-red{background:#ff7676 !important;color:#fff}.bg-red:hover{color:#fff}.bg-blue{background:#85b4f2 !important;color:#fff}.bg-blue:hover{color:#fff}.bg-yellow{background:#ffc107 !important;color:#fff}.bg-yellow:hover{color:#fff}.bg-green{background:#54e69d !important;color:#fff}.bg-green:hover{color:#fff}.bg-orange{background:#ffc36d !important;color:#fff}.bg-orange:hover{color:#fff}.bg-violet{background:#796AEE !important;color:#fff}.bg-violet:hover{color:#fff}.bg-gray{background:#ced4da !important}.bg-white{background:#fff !important}.text-red{color:#ff7676}.text-red:hover{color:#ff7676}.text-yellow{color:#ffc107}.text-yellow:hover{color:#ffc107}.text-green{color:#54e69d}.text-green:hover{color:#54e69d}.text-orange{color:#ffc36d}.text-orange:hover{color:#ffc36d}.text-violet{color:#796AEE}.text-violet:hover{color:#796AEE}.text-blue{color:#85b4f2}.text-blue:hover{color:#85b4f2}.text-gray{color:#999}.text-gray:hover{color:#999}.text-white{color:#fff}.no-padding{padding:0 !important}.no-padding-bottom{padding-bottom:0 !important}.no-padding-top{padding-top:0 !important}.no-margin{margin:0 !important}.no-margin-bottom{margin-bottom:0 !important}.no-margin-top{margin-top:0 !important}.padding-small{padding:100px 0}.btn{border-radius:0 !important}.badge{font-weight:300}.badge-rounded{border-radius:50px}.heading-light{font-weight:300 !important}.heading-medium{font-weight:400 !important}.pagination-template li.page-item{margin:0 5px}.pagination-template a.page-link{width:40px;height:40px;line-height:27px;border-radius:50% !important;border:1px solid #ddd;color:#555;text-align:center}.pagination-template a.page-link:hover, .pagination-template a.page-link.active{background:#f5f5f5}.text-primary{color:#379392 !important}p.text-hero{font-size:1.2em}p.text-hero i{font-size:1.2em}p.small-text-hero{font-size:1em}h1,h2,h3,h4,h5,h6{margin-bottom:15px}.pagination-template li.page-item{margin:0 5px}.pagination-template a.page-link{width:40px;height:40px;line-height:27px;border-radius:50% !important;border:none;color:#555;text-align:center}.pagination-template a.page-link:hover, .pagination-template a.page-link.active{background:#eee}.container-fluid{width:100%}.text-big{font-size:1.4em;font-weight:300;line-height:1.8em;color:#111}a.hero-link{color:inherit !important;text-transform:uppercase;font-size:1em;text-decoration:none !important;margin-top:20px;font-weight:300}a.hero-link::after{content:'';width:100%;height:1px;display:block;background:#fff;-webkit-transition:all 0.3s;transition:all 0.3s}a.hero-link:hover{color:#379392 !important}a.hero-link:hover::after{background:#379392}div[class*="-btn"]{cursor:pointer}@media (max-width:767px){nav.navbar .search-btn{margin-left:0;padding-left:0;border-left:none}}@media (max-width:575px){nav.navbar .container{width:100%}.breadcrumb li{display:block;width:100%;text-align:center}}.navbar{background:#fff;padding-top:20px !important;padding-bottom:20px !important;z-index:9998;border-bottom:1px solid #ddd}.navbar .langs a:first-of-type,.navbar .search-btn{margin-left:20px;padding-left:20px;border-left:1px solid #ddd;height:20px;line-height:20px}.navbar .search-btn{color:#333;font-size:0.9em}.navbar .navbar-toggler{margin-top:5px}.navbar .navbar-toggler span{width:20px;height:2px;background:#222;margin-bottom:4px;display:block}.navbar .navbar-toggler span:last-of-type{margin-bottom:0}.navbar .navbar-toggler.active span{margin:0}.navbar .navbar-toggler.active span:first-of-type{-webkit-transform:rotate(45deg) translate(3px);transform:rotate(45deg) translate(3px)}.navbar .navbar-toggler.active span:nth-of-type(2){opacity:0}.navbar .navbar-toggler.active span:last-of-type{-webkit-transform:rotate(-45deg) translate(3px);transform:rotate(-45deg) translate(3px)}.navbar .langs a{font-size:0.8em;color:#aaa;font-weight:700}.navbar .langs a.active{color:#333}.navbar .langs span{width:15px;height:1px;background:#ddd;margin:0 5px}.navbar .search-area{display:none;z-index:9999}.navbar .search-area-inner{position:fixed;top:0;right:0;width:100vw;height:100vh;background:rgba(255, 255, 255, 0.99);padding:20px !important}.navbar .search-area-inner .close-btn{position:absolute;top:20px;right:20px}.navbar .search-area-inner .row{width:100%}.navbar .search-area-inner .form-group{position:relative}.navbar .search-area-inner .submit{background:none;border:none;position:absolute;right:10px;bottom:15px}.navbar .search-area-inner input{width:100%;border:none;border-bottom:1px solid #ddd;background:none;padding:10px 0;font-size:1.6em;font-weight:300;font-family:"Open Sans", sans-serif}.navbar .search-area-inner input::-moz-placeholder{font-family:"Open Sans", sans-serif;color:#555;font-weight:300;font-size:1.1em}.navbar .search-area-inner input::-webkit-input-placeholder{font-family:"Open Sans", sans-serif;color:#555;font-weight:300;font-size:1.1em}.navbar .search-area-inner input:-ms-input-placeholder{font-family:"Open Sans", sans-serif;color:#555;font-weight:300;font-size:1.1em}.navbar .navbar-nav a.nav-link{color:#333;margin:0 5px;font-weight:400;font-size:0.95em}.navbar .navbar-nav a.nav-link:hover{color:#379392}.navbar .navbar-nav a.nav-link.active{color:#379392;font-weight:bold}@media (max-width:991px){nav.navbar .navbar-header{width:100%}nav.navbar::after{width:100%}nav.navbar.active::after{width:calc(100% - 170px)}nav.navbar .search-area{font-size:0.75em}}section.hero{padding:0;color:#fff;filter:brightness(80%)}section.hero .container{padding:200px 20px;position:relative}section.hero h1{line-height:1.2em}section.hero .continue{position:absolute;bottom:30px;left:20px;text-decoration:none !important;color:inherit !important;text-transform:uppercase;font-size:0.75em;opacity:0.8}section.hero .continue i{margin-right:5px}@media (max-width:767px){section.hero .container{padding:150px 20px}}section.featured-posts .row:nth-of-type(odd) .text{background:#fafafa}section.featured-posts .row:last-of-type{margin-bottom:0}section.featured-posts p{font-weight:400;color:#777;font-size:0.95em}section.featured-posts .text-inner{padding:70px 30px;height:100%;-webkit-transition:all 0.3s;transition:all 0.3s}section.featured-posts a{color:inherit;text-decoration:none}section.featured-posts h2{line-height:1.1em;color:#333;-webkit-transition:all 0.3s;transition:all 0.3s}section.featured-posts h2:hover{color:#555}section.featured-posts .avatar{max-width:40px;min-width:40px;height:40px;overflow:hidden;border-radius:50%;margin-right:10px}section.featured-posts .title,section.featured-posts .date,section.featured-posts .comments{font-size:0.8em;font-weight:400;color:#999}section.featured-posts .title i,section.featured-posts .date i,section.featured-posts .comments i{margin-right:5px}section.featured-posts .title::after,section.featured-posts .date::after,section.featured-posts .comments::after{content:'|';display:inline-block;margin:0 7px;font-size:0.9em;color:#ccc}section.featured-posts .comments::after{display:none}section.featured-posts .image{max-height:200px;width:auto;overflow:hidden;padding:0}section.featured-posts .image img{height:100%}section.featured-posts .post-header{margin-bottom:10px}section.featured-posts .post-footer{margin-top:30px}@media (max-width:991px){section.featured-posts .image{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1;max-height:300px;min-height:auto !important}section.featured-posts .image img{width:100%;height:auto !important}section.featured-posts .text{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}}@media (max-width:575px){section.featured-posts .post-footer{font-size:0.9em}}@media (max-width:350px){section.featured-posts .text-inner{padding:15px}section.featured-posts .post-footer{font-size:0.75em}}section.divider{color:#fff}section.latest-posts header{margin-bottom:50px}.post-meta{margin:10px 0;font-size:0.8em}.post-meta .date{text-transform:uppercase;font-weight:400}.post-meta .date::after{display:none}.post p:not(.lead){font-weight:400;color:#777;font-size:0.95em}.post a{text-decoration:none}.post a:hover, .post a:focus{text-decoration:none}.post h3{line-height:1.1em;color:#222;-webkit-transition:all 0.3s;transition:all 0.3s;margin-bottom:1rem}.post h3:hover{color:#555}.post .category a{color:#379392;letter-spacing:0.05em;font-weight:700;text-transform:uppercase;text-decoration:none}.post .date{letter-spacing:0.05em;font-weight:400;text-transform:uppercase;color:#aaa}.post .avatar{max-width:40px;min-width:40px;height:40px;overflow:hidden;border-radius:50%;margin-right:10px}.post .title,.post .date,.post .comments,.post .views{font-weight:400;color:#999;text-transform:capitalize}.post .title i,.post .date i,.post .comments i,.post .views i{margin-right:5px;font-size:1.1em}.post .title::after,.post .date::after,.post .comments::after,.post .views::after{content:'|';display:inline-block;margin:0 7px;font-size:0.9em;color:#ccc}.post .post-tags{margin-top:30px}.post .post-tags .tag{padding:5px 25px;border:1px solid #ddd;margin:5px;color:#777;font-size:0.75em;text-transform:uppercase;font-weight:600;text-decoration:none;border-radius:50px}.post .post-tags .tag:hover{background:#379392;color:#fff;border-color:#379392}.post .meta-last::after{display:none}@media (max-width:767px){.post{margin-bottom:40px}.post:last-of-type{margin-bottom:0}}section.newsletter .form-group{position:relative}section.newsletter input{width:100%;height:60px;line-height:60px;padding:0 15px;border:1px solid #111;font-size:1.1em;font-family:"Open Sans", sans-serif;font-weight:300}section.newsletter .submit{height:60px;padding:0 30px;line-height:60px;background:#111;border:1px solid #111;color:#fff;position:absolute;top:0;right:0;font-family:"Open Sans", sans-serif}@media (max-width:575px){section.newsletter input{font-size:0.95em;width:100%}section.newsletter .submit{position:static;width:100%;margin-top:10px}}section.gallery .mix{padding:0;max-height:200px;overflow:hidden}section.gallery a{outline:none;width:100%;height:100%}section.gallery a img{min-height:100%;width:100%}section.gallery .item{position:relative;width:100%;height:100%}section.gallery .item:hover .overlay{opacity:1}section.gallery .item:hover i{opacity:1;-webkit-transform:none;transform:none}section.gallery .overlay{position:absolute;top:0;right:0;width:100%;height:100%;background:rgba(55, 147, 146, 0.7);color:#fff;opacity:0;-webkit-transition:all 0.5s;transition:all 0.5s}section.gallery .overlay i{-webkit-transform:translateY(20px);transform:translateY(20px);-webkit-transition:all 0.5s;transition:all 0.5s;opacity:0;font-size:2em}footer.main-footer{background:#0e0e0e;padding:100px 0 0;color:#fff}footer.main-footer .contact-details p{font-weight:300;color:#fff;margin-bottom:5px;font-size:0.95em}footer.main-footer .contact-details a{text-decoration:underline;margin-bottom:0}footer.main-footer a{color:inherit;font-weight:300;margin-bottom:7px}footer.main-footer a:hover, footer.main-footer a:focus{color:#999}footer.main-footer ul{margin-right:30px}footer.main-footer .list-unstyled a{font-size:0.95em}footer.main-footer .latest-posts .image{max-width:50px;padding:4px;border:2px solid #333;margin-right:10px}footer.main-footer .latest-posts a{font-size:0.95em;text-decoration:none}footer.main-footer .latest-posts a:hover{color:#fff}footer.main-footer .latest-posts strong{display:block}footer.main-footer .latest-posts .date{font-size:0.85em;color:#aaa}footer.main-footer .copyrights{background:#090909;margin-top:100px;padding:20px 0;font-size:0.9em}footer.main-footer .copyrights *{margin-bottom:0}footer.main-footer .social-menu{margin-top:20px}footer.main-footer .social-menu li{padding:0 5px}footer.main-footer .date::after,footer.main-footer .title::after{display:none !important}footer.main-footer .footer-navigation{padding:20px 0}footer.main-footer .footer-nav{margin:0;padding:0;list-style:none}footer.main-footer .footer-nav li{margin:0 15px}footer.main-footer .footer-nav-link{color:#ccc;font-size:0.95em;font-weight:400;text-decoration:none;padding:8px 0;display:inline-block;transition:color 0.3s ease, border-bottom 0.3s ease;border-bottom:2px solid transparent}footer.main-footer .footer-nav-link:hover,footer.main-footer .footer-nav-link:focus{color:#fff;border-bottom-color:#379392}footer.main-footer .footer-divider{border:none;border-top:1px solid #333;margin:30px 0}footer.main-footer .social-menu{margin-top:20px}footer.main-footer .social-menu a{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:#2a2a2a;color:#ccc;font-size:16px;transition:all 0.3s ease}footer.main-footer .social-menu a:hover{background:#379392;color:#fff;transform:translateY(-3px)}@media (max-width:767px){footer.main-footer div[class*="col-"]{margin-bottom:40px}footer.main-footer div[class*="col-"]:last-of-type{margin-bottom:0}footer.main-footer .latest-posts>a{width:100%;margin-bottom:20px}footer.main-footer .copyrights div[class*="col-"]{margin-bottom:20px;text-align:center !important}footer.main-footer .copyrights div[class*="col-"]:last-of-type{margin-bottom:0}}main.posts-listing{padding-top:50px;padding-left:0;padding-right:0}main.posts-listing .post{margin-bottom:50px}main.posts-listing .post-footer{font-size:0.8em}.widget.search .form-group{position:relative}.widget.search input{width:100%;height:40px;line-height:40px;border:none;border-bottom:1px solid #ddd;font-size:0.95em;font-family:"Open Sans", sans-serif;font-weight:400;background:none}.widget.search input::-moz-placeholder{color:#aaa;font-family:"Open Sans", sans-serif}.widget.search input::-webkit-input-placeholder{color:#aaa;font-family:"Open Sans", sans-serif}.widget.search input:-ms-input-placeholder{color:#aaa;font-family:"Open Sans", sans-serif}.widget.search .submit{height:40px;padding:0;line-height:40px;background:none;border:none;color:#555;font-size:0.9em;position:absolute;top:0;right:0}.widget.latest-posts a{display:block;color:#555;text-decoration:none}.widget.latest-posts a:hover{color:#000}.widget.latest-posts .image{min-width:60px;max-width:60px;height:60px;overflow:hidden;margin-right:20px}.widget.latest-posts .item{margin-bottom:20px}.widget.latest-posts strong{font-size:0.95em;display:block;line-height:1em}.widget.latest-posts .views,.widget.latest-posts .comments{font-size:0.8em;font-weight:400;color:#bbb;margin-top:10px}.widget.latest-posts .views i,.widget.latest-posts .comments i{margin-right:5px}.widget.latest-posts .views::after,.widget.latest-posts .comments::after{content:'|';display:inline-block;margin:0 7px;font-size:0.9em;color:#ccc}.widget.latest-posts .comments::after{display:none}.widget.categories .item{background:#fafafa;padding:10px;color:#777;font-weight:700}.widget.categories .item:nth-of-type(2n+2){background:none}.widget.categories .item a{color:inherit;font-size:0.95em}.widget.categories .item a:hover{color:#000;text-decoration:none}.widget.categories .item span{font-size:0.9em;color:#aaa}.widget.tags .tag{padding:5px 25px;border:1px solid #ddd;margin:5px 0;color:#777;font-size:0.75em;text-transform:uppercase;font-weight:600;text-decoration:none;border-radius:50px}.widget.tags .tag:hover{background:#379392;color:#fff;border-color:#379392}.blog-post{padding-left:0;padding-right:0}.blog-post .post-footer{font-size:0.8em}.blog-post .post-thumbnail img{width:100%;margin-bottom:10px}.blog-post h1{color:#444;line-height:1.1em}.blog-post h1:hover{color:#444}.blog-post .post-footer{margin-top:20px}.blog-post .post-body{margin-top:40px}.blog-post .post-body h2,.blog-post .post-body h3,.blog-post .post-body h4,.blog-post .post-body h5,.blog-post .post-body h6{color:#333}.blog-post .post-body p{margin-bottom:30px}.blog-post .post-body p:not(.lead){font-size:1em;line-height:1.7em;color:#555}.blog-post .post-body img{max-width:100%;height:auto;display:block;margin:20px 0}.blog-post .posts-nav{margin-top:50px;color:#777;font-size:0.8em}.blog-post .posts-nav a{color:inherit;width:calc(50% - 10px);padding:10px 20px;border:1px solid #eee;margin-bottom:15px}.blog-post .posts-nav a:hover{border-color:#379392}.blog-post .posts-nav a:hover .icon{background:#379392;color:#fff;border-color:#379392}.blog-post .icon{min-width:35px;max-width:35px;height:35px;border-radius:50%;line-height:32px;border:1px solid #ddd;color:#aaa;font-size:1.5em;text-align:center;-webkit-transition:all 0.2s;transition:all 0.2s}.blog-post .icon.prev{margin-right:20px}.blog-post .icon.next{margin-left:20px}.blog-post .post-comments{margin-top:50px}.blog-post .post-comments span.no-of-comments{color:#777;font-size:0.8em;margin-left:5px;font-weight:400}.blog-post .post-comments header{margin-bottom:40px}.blog-post .post-comments .comment:last-of-type .comment-body{border-bottom:none}.blog-post .post-comments .image{margin-right:15px}.blog-post .post-comments .title::after{display:none}.blog-post .post-comments img{max-width:40px;min-width:40px;height:40px}.blog-post .post-comments strong{display:block;color:#555}.blog-post .post-comments span.date{font-size:0.8em;color:#999}.blog-post .post-comments span.date::after{display:none}.blog-post .post-comments .comment-body{margin-left:55px;margin-top:10px;margin-bottom:25px;padding-bottom:15px;border-bottom:1px solid #eee}.blog-post .post-comments p{font-size:0.95em;color:#555}.blog-post .add-comment{margin-top:50px}.blog-post .add-comment header{margin-bottom:30px}.blog-post .add-comment input,.blog-post .add-comment textarea{background:none;border:none;border-bottom:1px solid #ddd;padding:10px 0;border-radius:0;font-family:"Open Sans", sans-serif}.blog-post .add-comment input::-moz-placeholder,.blog-post .add-comment textarea::-moz-placeholder{font-weight:400;font-size:0.9em;color:#aaa;font-weight:400;font-family:"Open Sans", sans-serif}.blog-post .add-comment input::-webkit-input-placeholder,.blog-post .add-comment textarea::-webkit-input-placeholder{font-weight:400;font-size:0.9em;color:#aaa;font-weight:400;font-family:"Open Sans", sans-serif}.blog-post .add-comment input:-ms-input-placeholder,.blog-post .add-comment textarea:-ms-input-placeholder{font-weight:400;font-size:0.9em;color:#aaa;font-weight:400;font-family:"Open Sans", sans-serif}.blog-post .add-comment input:focus,.blog-post .add-comment textarea:focus{-webkit-box-shadow:none;box-shadow:none;border-bottom:1px solid #379392}.blog-post .add-comment textarea{min-height:150px}blockquote.blockquote{font-size:1.05em;line-height:1.7em;border-color:#379392;border:1px solid #eee;border-left:6px solid #eee;padding:20px;border-top-right-radius:5px;border-bottom-right-radius:5px;margin-bottom:30px}blockquote.blockquote p{margin-bottom:15px !important}@media (max-width:767px){.posts-nav a{width:100% !important}}@media (max-width:575px){.blog-post .title::after{display:none !important}.blog-post .author{margin-bottom:10px}}#style-switch-button{position:fixed;top:120px;left:0px;border-radius:0;z-index:100000}#style-switch{width:300px;padding:20px;position:fixed;top:160px;left:0;background:#fff;border:solid 1px #ced4da;z-index:100000}#style-switch h4{color:#495057}#style-switch .text-small{font-size:.8em}#style-switch select{font-size:0.85em}.navbar{padding:0.5rem 1rem}.navbar-brand{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem;margin-right:1rem;font-size:1rem;color:#333;font-weight:bold}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.25rem;line-height:1;border:1px solid transparent;border-radius:0.25rem}.navbar-light .navbar-brand{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0, 0, 0, 0.5)}.navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover{color:rgba(0, 0, 0, 0.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0, 0, 0, 0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:rgba(0, 0, 0, 0.9)}.navbar-light .navbar-toggler{color:rgba(0, 0, 0, 0.5);border-color:rgba(0, 0, 0, 0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0, 0, 0, 0.5)}.navbar-dark .navbar-brand{color:white}.navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover{color:white}.navbar-dark .navbar-nav .nav-link{color:rgba(255, 255, 255, 0.5)}.navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover{color:rgba(255, 255, 255, 0.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255, 255, 255, 0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:white}.navbar-dark .navbar-toggler{color:rgba(255, 255, 255, 0.5);border-color:rgba(255, 255, 255, 0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255, 255, 255, 0.5)}.btn{font-weight:normal;border:1px solid transparent;padding:0.5rem 1rem;font-size:1rem;line-height:1.25;border-radius:0.25rem;-webkit-transition:all 0.15s ease-in-out;transition:all 0.15s ease-in-out}.btn:focus, .btn.focus{outline:0;-webkit-box-shadow:0 0 0 3px rgba(55, 147, 146, 0.25);box-shadow:0 0 0 3px rgba(55, 147, 146, 0.25)}.btn:active, .btn.active{background-image:none}.btn-primary{color:#fff;background-color:#379392;border-color:#379392}.btn-primary:hover{color:#fff;background-color:#2d7776;border-color:#296e6d}.btn-primary:focus, .btn-primary.focus{-webkit-box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5);box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5)}.btn-primary.disabled, .btn-primary:disabled{background-color:#379392;border-color:#379392}.btn-primary:active, .btn-primary.active,.show>.btn-primary.dropdown-toggle{background-color:#2d7776;background-image:none;border-color:#296e6d}.btn-secondary{color:#fff;background-color:#868e96;border-color:#868e96}.btn-secondary:hover{color:#fff;background-color:#727b84;border-color:#6c757d}.btn-secondary:focus, .btn-secondary.focus{-webkit-box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5);box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5)}.btn-secondary.disabled, .btn-secondary:disabled{background-color:#868e96;border-color:#868e96}.btn-secondary:active, .btn-secondary.active,.show>.btn-secondary.dropdown-toggle{background-color:#727b84;background-image:none;border-color:#6c757d}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success:focus, .btn-success.focus{-webkit-box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5);box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5)}.btn-success.disabled, .btn-success:disabled{background-color:#28a745;border-color:#28a745}.btn-success:active, .btn-success.active,.show>.btn-success.dropdown-toggle{background-color:#218838;background-image:none;border-color:#1e7e34}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info:focus, .btn-info.focus{-webkit-box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5);box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5)}.btn-info.disabled, .btn-info:disabled{background-color:#17a2b8;border-color:#17a2b8}.btn-info:active, .btn-info.active,.show>.btn-info.dropdown-toggle{background-color:#138496;background-image:none;border-color:#117a8b}.btn-warning{color:#111;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#111;background-color:#e0a800;border-color:#d39e00}.btn-warning:focus, .btn-warning.focus{-webkit-box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5);box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5)}.btn-warning.disabled, .btn-warning:disabled{background-color:#ffc107;border-color:#ffc107}.btn-warning:active, .btn-warning.active,.show>.btn-warning.dropdown-toggle{background-color:#e0a800;background-image:none;border-color:#d39e00}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger:focus, .btn-danger.focus{-webkit-box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5);box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5)}.btn-danger.disabled, .btn-danger:disabled{background-color:#dc3545;border-color:#dc3545}.btn-danger:active, .btn-danger.active,.show>.btn-danger.dropdown-toggle{background-color:#c82333;background-image:none;border-color:#bd2130}.btn-light{color:#111;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#111;background-color:#e2e6ea;border-color:#dae0e5}.btn-light:focus, .btn-light.focus{-webkit-box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5);box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5)}.btn-light.disabled, .btn-light:disabled{background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:active, .btn-light.active,.show>.btn-light.dropdown-toggle{background-color:#e2e6ea;background-image:none;border-color:#dae0e5}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark:focus, .btn-dark.focus{-webkit-box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5);box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5)}.btn-dark.disabled, .btn-dark:disabled{background-color:#343a40;border-color:#343a40}.btn-dark:active, .btn-dark.active,.show>.btn-dark.dropdown-toggle{background-color:#23272b;background-image:none;border-color:#1d2124}.btn-outline-primary{color:#379392;background-color:transparent;background-image:none;border-color:#379392}.btn-outline-primary:hover{color:#fff;background-color:#379392;border-color:#379392}.btn-outline-primary:focus, .btn-outline-primary.focus{-webkit-box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5);box-shadow:0 0 0 3px rgba(55, 147, 146, 0.5)}.btn-outline-primary.disabled, .btn-outline-primary:disabled{color:#379392;background-color:transparent}.btn-outline-primary:active, .btn-outline-primary.active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#379392;border-color:#379392}.btn-outline-secondary{color:#868e96;background-color:transparent;background-image:none;border-color:#868e96}.btn-outline-secondary:hover{color:#fff;background-color:#868e96;border-color:#868e96}.btn-outline-secondary:focus, .btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5);box-shadow:0 0 0 3px rgba(134, 142, 150, 0.5)}.btn-outline-secondary.disabled, .btn-outline-secondary:disabled{color:#868e96;background-color:transparent}.btn-outline-secondary:active, .btn-outline-secondary.active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#868e96;border-color:#868e96}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:focus, .btn-outline-success.focus{-webkit-box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5);box-shadow:0 0 0 3px rgba(40, 167, 69, 0.5)}.btn-outline-success.disabled, .btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:active, .btn-outline-success.active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:focus, .btn-outline-info.focus{-webkit-box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5);box-shadow:0 0 0 3px rgba(23, 162, 184, 0.5)}.btn-outline-info.disabled, .btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:active, .btn-outline-info.active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#fff;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:focus, .btn-outline-warning.focus{-webkit-box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5);box-shadow:0 0 0 3px rgba(255, 193, 7, 0.5)}.btn-outline-warning.disabled, .btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:active, .btn-outline-warning.active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#ffc107;border-color:#ffc107}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:focus, .btn-outline-danger.focus{-webkit-box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5);box-shadow:0 0 0 3px rgba(220, 53, 69, 0.5)}.btn-outline-danger.disabled, .btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:active, .btn-outline-danger.active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#fff;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:focus, .btn-outline-light.focus{-webkit-box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5);box-shadow:0 0 0 3px rgba(248, 249, 250, 0.5)}.btn-outline-light.disabled, .btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:active, .btn-outline-light.active,.show>.btn-outline-light.dropdown-toggle{color:#fff;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:focus, .btn-outline-dark.focus{-webkit-box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5);box-shadow:0 0 0 3px rgba(52, 58, 64, 0.5)}.btn-outline-dark.disabled, .btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:active, .btn-outline-dark.active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-lg{padding:0.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:0.3rem}.btn-sm{padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5;border-radius:0.2rem}body{font-family:"Open Sans", sans-serif;font-size:1rem;font-weight:normal;line-height:1.5;color:#212529;background-color:#fff}a{color:#379392;text-decoration:none}a:focus, a:hover{color:#225b5b;text-decoration:underline}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-family:inherit;font-weight:700;line-height:1.1;color:inherit}h1,.h1{font-size:2.5rem;color:#379392}h2,.h2{font-size:2rem}h3,.h3{font-size:1.75rem}h4,.h4{font-size:1.5rem}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}.lead{font-size:1.3rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.1}.display-2{font-size:5.5rem;font-weight:300;line-height:1.1}.display-3{font-size:4.5rem;font-weight:300;line-height:1.1}.display-4{font-size:3.5rem;font-weight:300;line-height:1.1}hr{border-top:1px solid rgba(0, 0, 0, 0.1)}small,.small{font-size:80%;font-weight:normal}mark,.mark{padding:0.2em;background-color:#fcf8e3}.blockquote{padding:0.5rem 1rem;margin-bottom:1rem;font-size:1.25rem;border-left:5px solid #379392}.blockquote-footer{color:#868e96}.blockquote-footer::before{content:"\2014 \00A0"}.text-primary{color:#379392 !important}a.text-primary:focus, a.text-primary:hover{color:#296e6d !important}.page-item:first-child .page-link{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{color:#fff;background-color:#379392;border-color:#379392}.page-item.disabled .page-link{color:#868e96;background-color:#fff;border-color:#ddd}.page-link{padding:0.5rem 0.75rem;line-height:1.25;color:#379392;background-color:#fff;border:1px solid #ddd}.page-link:focus, .page-link:hover{color:#225b5b;text-decoration:none;background-color:#e9ecef;border-color:#ddd}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.bg-primary{background-color:#379392 !important}a.bg-primary:focus, a.bg-primary:hover{background-color:#296e6d !important}.bg-secondary{background-color:#868e96 !important}a.bg-secondary:focus, a.bg-secondary:hover{background-color:#6c757d !important}.bg-success{background-color:#28a745 !important}a.bg-success:focus, a.bg-success:hover{background-color:#1e7e34 !important}.bg-info{background-color:#17a2b8 !important}a.bg-info:focus, a.bg-info:hover{background-color:#117a8b !important}.bg-warning{background-color:#ffc107 !important}a.bg-warning:focus, a.bg-warning:hover{background-color:#d39e00 !important}.bg-danger{background-color:#dc3545 !important}a.bg-danger:focus, a.bg-danger:hover{background-color:#bd2130 !important}.bg-light{background-color:#f8f9fa !important}a.bg-light:focus, a.bg-light:hover{background-color:#dae0e5 !important}.bg-dark{background-color:#343a40 !important}a.bg-dark:focus, a.bg-dark:hover{background-color:#1d2124 !important}.border-primary{border-color:#379392 !important}.border-secondary{border-color:#868e96 !important}.border-success{border-color:#28a745 !important}.border-info{border-color:#17a2b8 !important}.border-warning{border-color:#ffc107 !important}.border-danger{border-color:#dc3545 !important}.border-light{border-color:#f8f9fa !important}.border-dark{border-color:#343a40 !important}.text-primary{color:#379392 !important}a.text-primary:focus, a.text-primary:hover{color:#296e6d !important}.text-secondary{color:#868e96 !important}a.text-secondary:focus, a.text-secondary:hover{color:#6c757d !important}.text-success{color:#28a745 !important}a.text-success:focus, a.text-success:hover{color:#1e7e34 !important}.text-info{color:#17a2b8 !important}a.text-info:focus, a.text-info:hover{color:#117a8b !important}.text-warning{color:#ffc107 !important}a.text-warning:focus, a.text-warning:hover{color:#d39e00 !important}.text-danger{color:#dc3545 !important}a.text-danger:focus, a.text-danger:hover{color:#bd2130 !important}.text-light{color:#f8f9fa !important}a.text-light:focus, a.text-light:hover{color:#dae0e5 !important}.text-dark{color:#343a40 !important}a.text-dark:focus, a.text-dark:hover{color:#1d2124 !important} \ No newline at end of file diff --git a/src/public/themes/blog/footer.php b/src/public/themes/blog/footer.php index 807e9543e..ba42d9c62 100755 --- a/src/public/themes/blog/footer.php +++ b/src/public/themes/blog/footer.php @@ -1,58 +1,76 @@