diff --git a/.gitea/workflows/deploy-test.yml b/.gitea/workflows/deploy-test.yml
index 365abc0..9f7ce49 100755
--- a/.gitea/workflows/deploy-test.yml
+++ b/.gitea/workflows/deploy-test.yml
@@ -1,131 +1,123 @@
-name: Deploy Feature Branch to Test
+name: Deploy Feature Branch to Test (haus.test.kies-media.de)
on:
push:
branches:
- "feature/**"
+ workflow_dispatch:
+ inputs:
+ ref:
+ description: "Branch or tag to deploy (default: HEAD)"
+ required: false
+ default: ""
jobs:
- lint-php:
- name: PHP Syntax Check
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Install PHP
- run: apt-get update -qq && apt-get install -y -qq php-cli > /dev/null 2>&1
-
- - name: PHP Lint
- run: |
- errors=0
- while IFS= read -r file; do
- if ! php -l "$file" > /dev/null 2>&1; then
- echo "❌ Syntax error in $file"
- php -l "$file"
- errors=1
- fi
- done < <(find . -name "*.php" -not -path "./vendor/*")
- if [ "$errors" -eq 1 ]; then
- echo "::error::PHP lint check failed"
- exit 1
- fi
- echo "✅ All PHP files pass syntax check"
-
- lint-css:
- name: CSS Lint (stylelint)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Install Node.js & stylelint
- run: |
- apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
- npm install -g stylelint stylelint-config-standard stylelint-prettier > /dev/null 2>&1
-
- - name: CSS Lint
- run: |
- npx stylelint "**/*.css" --config .stylelintrc.json --allow-empty-input
- echo "✅ All CSS files pass lint"
-
- lint-html:
- name: HTML Lint (htmlhint)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Install Node.js & htmlhint
- run: |
- apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
- npm install -g htmlhint > /dev/null 2>&1
-
- - name: HTML Lint
- run: |
- npx htmlhint "**/*.html" --config .htmlhintrc
- echo "✅ All HTML files pass lint"
-
deploy:
name: Deploy to Test Environment
runs-on: ubuntu-latest
- needs: [lint-php, lint-css, lint-html]
- container:
- volumes:
- - /var/www/test/html:/deploy
+ concurrency:
+ group: deploy-test
+ cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
- - name: Show branch info
+ - name: Setup SSH
run: |
- echo "=== Deploying branch: ${{ gitea.ref_name }} ==="
- echo "=== Commit: ${{ gitea.sha }} ==="
- echo "=== By: ${{ gitea.actor }} ==="
- echo "=== All lint checks passed ✅ ==="
- date
+ mkdir -p ~/.ssh
+ echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ ssh-keyscan -H 188.245.242.194 >> ~/.ssh/known_hosts 2>/dev/null
+ echo "✅ SSH key configured"
- - name: Deploy to test environment
+ - name: Verify SSH connectivity
run: |
- echo "Syncing files to test environment..."
- apt-get update -qq && apt-get install -y -qq rsync > /dev/null 2>&1 || true
-
- rsync -av --delete \
+ ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \
+ haustest@188.245.242.194 "echo 'SSH-OK as:' \$(whoami) 'on' \$(hostname)"
+
+ - name: Backup current test deployment
+ run: |
+ ssh -i ~/.ssh/id_ed25519 haustest@188.245.242.194 \
+ "cd /home/haustest/htdocs && \
+ tar czf /home/haustest/backup-pre-deploy-\$(date +%Y%m%d-%H%M%S).tar.gz \
+ haus.test.kies-media.de && \
+ ls -lh /home/haustest/backup-pre-deploy-*.tar.gz | tail -1"
+
+ - name: Rsync to test environment
+ run: |
+ rsync -avz --delete \
--exclude='.git' \
+ --exclude='node_modules' \
+ --exclude='tests' \
+ --exclude='docs' \
--exclude='.gitea' \
- --exclude='.husky' \
- --exclude='.prettierrc' \
- --exclude='.prettierignore' \
- --exclude='.stylelintrc.json' \
- --exclude='.htmlhintrc' \
- --exclude='.gitignore' \
- --exclude='.dockerignore' \
--exclude='.continue' \
+ --exclude='.husky' \
--exclude='Dockerfile' \
+ --exclude='.dockerignore' \
--exclude='nginx.conf' \
--exclude='eslint.config.js' \
--exclude='package.json' \
--exclude='package-lock.json' \
- --exclude='docs/' \
+ --exclude='phpunit.xml' \
+ --exclude='scripts' \
--exclude='AGENTS.md' \
--exclude='README.md' \
- --exclude='scripts/' \
- ./ /deploy/
-
+ --exclude='CLAUDE.md' \
+ --exclude='*.md' \
+ --exclude='.htmlhintrc' \
+ --exclude='.prettierrc' \
+ --exclude='.prettierignore' \
+ --exclude='.stylelintrc.json' \
+ --exclude='.editorconfig' \
+ --exclude='.well-known' \
+ -e "ssh -i ~/.ssh/id_ed25519" \
+ ./ haustest@188.245.242.194:/home/haustest/htdocs/haus.test.kies-media.de/
- echo "✅ Deployment complete!"
-
- - name: Set permissions
+ - name: Smoke test
run: |
- chown -R 33:33 /deploy/ 2>/dev/null || true
- chmod -R 755 /deploy/ 2>/dev/null || true
- echo "✅ Permissions set"
+ sleep 2
+ echo "--- HTTP status codes ---"
+ for path in "/" "/impressum" "/datenschutz"; do
+ code=$(curl -s -o /dev/null -w "%{http_code}" \
+ -H "Cache-Control: no-cache" \
+ "https://haus.test.kies-media.de${path}")
+ echo " $path → HTTP $code"
+ if [ "$code" != "200" ]; then
+ echo "❌ Smoke test failed for $path"
+ exit 1
+ fi
+ done
+ echo ""
+ echo "--- Locale switcher present? ---"
+ if curl -sL "https://haus.test.kies-media.de/" | grep -q "class=\"locale-switcher\""; then
+ echo " ✅ Locale switcher rendered"
+ else
+ echo " ❌ Locale switcher MISSING"
+ exit 1
+ fi
+ echo ""
+ echo "--- All 4 locales serving? ---"
+ for loc in de en uk ru; do
+ lang=$(curl -sL -H "Cache-Control: no-cache" \
+ -b "locale=$loc" \
+ "https://haus.test.kies-media.de/" \
+ | grep -oE '> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Target:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY
+ echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- **Server:** haustest@188.245.242.194" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**Review URL:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY
diff --git a/app/Controllers/LocaleSwitcher.php b/app/Controllers/LocaleSwitcher.php
index 25a35ff..42cd0cb 100644
--- a/app/Controllers/LocaleSwitcher.php
+++ b/app/Controllers/LocaleSwitcher.php
@@ -36,7 +36,8 @@ final class LocaleSwitcher
'UTF-8',
);
- $html = '
';
+ $html = '';
+ $html .= '
';
foreach (Locale::SUPPORTED as $code) {
$isCurrent = $code === $this->currentLocale;
$name = htmlspecialchars(
@@ -74,6 +75,53 @@ final class LocaleSwitcher
}
$html .= '
';
+ // Mobile dropdown — compact single-trigger switcher for narrow viewports
+ $currentName = htmlspecialchars(
+ I18n::t('locale.' . $this->currentLocale, [], $this->currentLocale),
+ ENT_QUOTES,
+ 'UTF-8',
+ );
+ $currentCode = htmlspecialchars($this->currentLocale, ENT_QUOTES, 'UTF-8');
+ $currentFlag = self::flagSvg($this->currentLocale);
+ $html .= '
';
+ $html .= '';
+ $html .= '';
+ $html .= $currentFlag;
+ $html .= '' . strtoupper($currentCode) . '';
+ $html .= '';
+ $html .= '▾';
+ $html .= '
';
+ $html .= '';
+ $html .= ' ';
+ $html .= '
';
+
return $html;
}
diff --git a/public/css/haus-schleusingen.css b/public/css/haus-schleusingen.css
index 02e8f32..4267f58 100755
--- a/public/css/haus-schleusingen.css
+++ b/public/css/haus-schleusingen.css
@@ -54,6 +54,7 @@ a:focus-visible {
--charcoal: #2e2b26;
--accent: #8b6914;
--accent-light: #c49a2a;
+ --accent-strong: #5a450d;
--white: #fdfcfa;
}
@@ -338,7 +339,7 @@ nav.scrolled .nav-hamburger span::after {
gap: 2.5rem;
align-items: center;
font-size: 0.82rem;
- color: rgb(255 255 255 / 60%);
+ color: rgb(255 255 255 / 88%);
letter-spacing: 0.05em;
}
@@ -441,10 +442,10 @@ nav.scrolled .nav-hamburger span::after {
.section-eyebrow {
font-size: 0.72rem;
- font-weight: 500;
+ font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
- color: var(--accent);
+ color: var(--accent-strong);
margin-bottom: 1rem;
}
@@ -812,15 +813,21 @@ nav.scrolled .nav-hamburger span::after {
font-size: 0.72rem;
letter-spacing: 0.15em;
text-transform: uppercase;
- color: var(--stone);
+ color: rgb(255 255 255 / 70%);
margin-bottom: 1rem;
}
-.price-card.highlight .pc-label {
+.price-card .price-label {
+ display: block;
+ font-size: 0.72rem;
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
color: rgb(255 255 255 / 70%);
+ margin-bottom: 0.8rem;
}
-.pc-val {
+.price-card .price-value {
+ display: block;
font-family: "Cormorant Garamond", serif;
font-size: 2.8rem;
font-weight: 600;
@@ -829,15 +836,37 @@ nav.scrolled .nav-hamburger span::after {
margin-bottom: 0.3rem;
}
-.pc-sub {
+.price-card .price-unit {
+ display: block;
font-size: 0.78rem;
- color: var(--stone);
+ color: rgb(255 255 255 / 75%);
}
-.price-card.highlight .pc-sub {
+.price-card.highlight .price-label,
+.price-card.highlight .price-unit {
+ color: rgb(255 255 255 / 85%);
+}
+
+.pricing-section .rent-notes {
+ margin-top: 2.5rem;
+ display: grid;
+ grid-template-columns: 12rem 1fr;
+ gap: 0.5rem 1.5rem;
+}
+
+.pricing-section .rent-notes dt {
+ font-size: 0.78rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
color: rgb(255 255 255 / 70%);
}
+.pricing-section .rent-notes dd {
+ margin: 0;
+ color: var(--white);
+ font-size: 0.95rem;
+}
+
.price-note {
margin-top: 2.5rem;
padding-top: 2.5rem;
@@ -1210,6 +1239,24 @@ footer {
display: flex;
}
+ /* Logo: keep icon, hide text on small viewports */
+ .logo-text {
+ display: none;
+ }
+
+ /* Locale switcher: replace 4-flag list with single-trigger dropdown */
+ .locale-switcher {
+ display: none;
+ }
+
+ .locale-switcher-mobile {
+ display: block;
+ }
+
+ .locale-switcher-mobile[open] > .locale-switcher-mobile__menu {
+ display: block;
+ }
+
/* Mobile slide-down nav */
nav.mobile-open .nav-links {
display: flex;
@@ -1324,14 +1371,16 @@ footer {
}
}
-/* LOCALE SWITCHER (sub-Issue D) */
-.locale-switcher {
- display: flex;
- align-items: center;
- gap: 0.25rem;
- list-style: none;
- margin: 0;
- padding: 0;
+/* LOCALE SWITCHER (sub-Issue D) — desktop only */
+@media (width > 900px) {
+ .locale-switcher {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
}
.locale-switcher__item {
@@ -1443,6 +1492,110 @@ nav.scrolled .locale-switcher__option.is-current {
border: 0;
}
+/* MOBILE LOCALE SWITCHER (dropdown — defaults to hidden on desktop) */
+@media (width > 900px) {
+ .locale-switcher-mobile {
+ display: none;
+ }
+}
+
+.locale-switcher-mobile {
+ position: relative;
+}
+
+.locale-switcher-mobile__trigger {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.55rem 0.7rem;
+ border: 1px solid rgb(0 0 0 / 12%);
+ border-radius: 6px;
+ background: rgb(255 255 255 / 95%);
+ cursor: pointer;
+ list-style: none;
+ min-height: 44px;
+ color: var(--dark);
+ font-size: 0.78rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+}
+
+.locale-switcher-mobile__trigger::-webkit-details-marker {
+ display: none;
+}
+
+.locale-switcher-mobile__current {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.locale-switcher-mobile__current-code {
+ letter-spacing: 0.05em;
+}
+
+.locale-switcher-mobile__caret {
+ font-size: 0.7rem;
+ transition: transform 0.2s ease;
+}
+
+.locale-switcher-mobile[open] .locale-switcher-mobile__caret {
+ transform: rotate(180deg);
+}
+
+.locale-switcher-mobile__menu {
+ position: absolute;
+ top: calc(100% + 6px);
+ right: 0;
+ min-width: 180px;
+ margin: 0;
+ padding: 0.4rem;
+ list-style: none;
+ background: var(--white);
+ border: 1px solid var(--warm);
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
+ z-index: 60;
+ display: none;
+}
+
+.locale-switcher-mobile__option {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ padding: 0.6rem 0.7rem;
+ border-radius: 4px;
+ text-decoration: none;
+ color: var(--dark);
+ font-size: 0.85rem;
+ min-height: 44px;
+}
+
+.locale-switcher-mobile__option.is-current {
+ background: var(--cream);
+ color: var(--accent-strong);
+ font-weight: 600;
+}
+
+.locale-switcher-mobile__option:hover,
+.locale-switcher-mobile__option:focus-visible {
+ background: var(--warm);
+ outline: none;
+}
+
+nav.scrolled .locale-switcher-mobile__trigger {
+ background: var(--white);
+ color: var(--dark);
+ border-color: var(--warm);
+}
+
+nav:not(.scrolled) .locale-switcher-mobile__trigger {
+ background: rgb(255 255 255 / 12%);
+ color: var(--white);
+ border-color: rgb(255 255 255 / 30%);
+ backdrop-filter: blur(4px);
+}
+
/* FORM FIELD ERRORS (sub-Issue E) */
.form-field-error {
margin: 0.375rem 0 0;
diff --git a/tests/Controllers/LocaleSwitcherTest.php b/tests/Controllers/LocaleSwitcherTest.php
index 2db00e8..ff5e532 100644
--- a/tests/Controllers/LocaleSwitcherTest.php
+++ b/tests/Controllers/LocaleSwitcherTest.php
@@ -24,6 +24,7 @@ final class LocaleSwitcherTest extends TestCase
{
$html = (new LocaleSwitcher('en', '/'))->render();
self::assertStringContainsString('. The active
// locale renders as (no hreflang). Together all
@@ -34,7 +35,10 @@ final class LocaleSwitcherTest extends TestCase
"locale '$code' is missing from switcher",
);
}
- self::assertSame(4, substr_count($html, 'class="flag"'), 'expected 4 flag SVGs');
+ // Both the desktop list and the mobile dropdown render a flag per locale
+ self::assertSame(4, substr_count($html, 'class="locale-switcher__option'), 'expected 4 desktop options');
+ self::assertSame(4, substr_count($html, 'class="locale-switcher-mobile__option'), 'expected 4 mobile options');
+ self::assertSame(9, substr_count($html, 'class="flag"'), 'expected 9 flag SVGs (4 desktop + 4 mobile menu + 1 mobile trigger)');
}
#[Test]