Compare commits

..

14 Commits

Author SHA1 Message Date
Disco DeDisco
3800c5bdad fixed attribution of .fa-hand-pointer cursor color scheme to ordering according to token-drop sequence instead of seat sequence; updates to accomodate this throughout apps.epic.models & .views, plus new apps.epic migration; assigned #id_sig_cursor_portal a z-index value corresponding to a high position but still beneath the #id_tray apparatus; minor semantic reordering of INSTALLED_APPS in core.settings
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-08 22:53:44 -04:00
Disco DeDisco
12d575a84b fixed seeding problem w. setUp helper causing same FTs to persistently fail
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-08 13:34:22 -04:00
Disco DeDisco
c14b6d7062 fixed some old data in two pipeline errors pointing to new Middle Arcana labels still as Minor Arcana
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-08 12:51:46 -04:00
Disco DeDisco
a7c5468cbc fixed failing channels FT related to Sig select; FT fix only, code written as intended
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-08 12:18:20 -04:00
Disco DeDisco
4da8750c60 fixed tooltip illegibility due to similar color to bg on .sig-overlay when data-polarity='gravity'
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2026-04-08 11:57:44 -04:00
Disco DeDisco
cf40f626e6 Sig select: _card-deck.scss extract, WS cursor fixes, own-role indicators, role icon refresh
- New _card-deck.scss: sig select styles moved out of _room.scss + _game-kit.scss
- sig-select.js: 3 WS bug fixes — thumbs-up deferred to window.load (layout settled
  before getBoundingClientRect), hover cursor cleared for all cards on reservation
  (not just the reserved card), applyHover guards against already-reserved roles
- Own-role indicators: gamer now sees their own role-coloured card outline + thumbs-up
- Reservation glow: replaced blurry role+ninUser double-shadow with crisp 2px outline
- Gravity qualifier: Graven text set to --terUser (matches Leavened/--quiUser pattern)
- Role card SVGs refreshed; starter-role-Blank removed
- FTs + Jasmine specs extended for sig select WS behaviour
- setup_sig_session management command for multi-browser manual testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 11:52:49 -04:00
Disco DeDisco
99a826f6c9 FT: pin AppletMenuDismissTest to portrait viewport (800×1200)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Landscape layout activates sidebar CSS which causes #id_dash_content to
overlap the base-template h2 in CI headless Firefox, triggering
ElementClickInterceptedException. Portrait viewport sidesteps all
landscape breakpoints so the h2 sits safely above #id_dash_content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 01:26:35 -04:00
Disco DeDisco
51fe2614fa overruling other scss specificity in .btn-disabled 2026-04-07 00:43:26 -04:00
Disco DeDisco
56dc094b45 Jasmine: fix 2 failing specs, drop 5 always-pending touch specs
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- FYI btn is now btn-disabled when caution open; rename test to assert
  disabled click does NOT close caution (old toggle expectation was stale)
- Hover-resets-is-reversed: cloneNode post-init has no mouseenter listener
  (direct binding, not delegation); use mouseleave + re-enter on same card
- Remove 3 touch describe blocks (5 specs total); TouchEvent unavailable
  in desktop Firefox means they never ran; touch behaviour covered by FTs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:36:28 -04:00
Disco DeDisco
520fdf7862 Sig select: caution tooltip, FLIP/FYI stat block, keyword display
- TarotCard.cautions JSONField + cautions_json property; migrations
  0027–0029 seed The Schizo (number=1) with 4 rival-interaction cautions
  (Roman-numeral card refs: I. The Pervert / II. The Occultist, etc.)
- Sig-select overlay: FLIP (stat-block toggle) + FYI (caution tooltip)
  buttons; nav PRV/NXT portaled outside tooltip at bottom corners (z-70);
  caution tooltip covers stat block (inset:0, z-60, Gaussian blur);
  tooltip click dismisses; FLIP/FYI fully dead while btn-disabled;
  nav wraps circularly (4/4 → 1/4, 1/4 → 4/4)
- SCSS: btn-disabled specificity fix (!important); btn-nav-left/right
  classes; sig-caution-* layout; stat-face keyword lists
- Jasmine suite expanded: stat block + FLIP (5 specs), caution tooltip
  (16 specs) including wrap-around and disabled-button behaviour
- IT tests: TarotCardCautionsTest (5), SigSelectRenderingTest (8)
- Role-card SVG icons added to static/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 00:22:04 -04:00
Disco DeDisco
e2cc38686f XL landscape: revert tray to landscape style; fix sig-stage stretch
- Remove _tray.scss XL (≥1800px) portrait override block entirely
- _isLandscape() no longer returns false at ≥1800px — tray uses
  landscape slide-from-top at all wide landscape widths
- sig-stage: align-self: stretch (was center) so JS sizeSigCard()
  measures correct stage width; card size no longer collapses
- Position strip: horizontal row at top (was vertical column-reverse)
- sig-overlay/sig-stage/sig-deck-grid layout polish at 1100px/1800px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 20:11:24 -04:00
Disco DeDisco
0bcc7567bb XL landscape polish: btn-primary sizing, tray from right, footer bg, layout fixes
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- .btn-xl removed; .btn-primary absorbs 4rem sizing (same as PICK SIGS/PICK ROLES)
- Landscape navbar .btn-primary: 3rem → 4rem to match base; XL stays 4rem (consistent)
- _button-pad.scss XL: base .btn ×1.2 (2.4rem); .btn-xl block deleted
- _tray.scss XL (≥1800px): portrait-style tray (slides from right, z-95)
- tray.js: _isLandscape() returns false at ≥1800px; portrait code paths run throughout
- Footer sidebar: background-color added so opaque footer masks tray sliding behind it
- Copyright .footer-container: bottom → top in landscape sidebar
- #id_room_menu: right: 2.5rem override in _room.scss XL block (cascade fix)
- navbar-text XL: 0.65rem × 1.2 = 0.78rem
- All landscape media queries: max-width: 1440px cutoff removed (already done prior)
- btn-xl class stripped from all 5 templates; test_navbar.py assertion updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 03:02:37 -04:00
Disco DeDisco
6654785f25 XL landscape breakpoint (≥1800px): double sidebar widths + scale content
- _base.scss: new @media (orientation:landscape) and (min-width:1800px) block —
  sidebars 4rem→8rem; navbar btn 3rem→5rem; brand h1 1.2rem→2.4rem; navbar-text
  0.65rem→1.3rem; footer icons 1.75rem→3rem; nav gap 3rem→4rem; footer-container
  0.55rem→0.85rem; container margins 4rem→8rem; h2 portrait-style (2rem, centred)
- _applets.scss: gear btn right 0.5rem→2.5rem; menus right 0.5rem→2rem at ≥1800px
- _game-kit.scss: kit btn right 0.5rem→2.5rem at ≥1800px
- _room.scss: sig-overlay padding-left 4rem→8rem at ≥1800px
- _tray.scss: tray wrap left/right 4rem→8rem at ≥1800px
- room.js: sizeSigModal right inset 64px→128px at ≥1800px viewport width

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 01:41:18 -04:00
Disco DeDisco
99a69202b9 landscape layout: remove max-width cutoff; sig-select stage/grid polish
- All landscape @media queries: drop and (max-width: 1440px) — sidebar layout
  now activates for all landscape orientations regardless of viewport width
- _base.scss landscape container: add max-width:none to override the
  @media(min-width:1200px) rule and fill the full space between sidebars
- sig-select sig-deck-grid: landscape now 9×2 @ 3rem cards; 18×1 at ≥1100px
  (bumped from 992px to avoid last-card clip); card text scales with --sig-card-w
- sig-stat-block: flex:1→flex:0 0 auto with width:--sig-card-w so it matches
  preview card dimensions instead of stretching across the full stage
- room.js sizeSigModal: landscape card width clamped to [90px, 160px]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 01:30:31 -04:00
43 changed files with 3406 additions and 736 deletions

View File

@@ -0,0 +1,154 @@
"""
Data migration — Earthman deck:
1. Rename three suit codes (and card names) for Earthman cards:
WANDS → BRANDS (Wands → Brands)
CUPS → GRAILS (Cups → Grails)
SWORDS → BLADES (Swords → Blades)
CROWNS stays CROWNS.
2. Copy keywords_upright / keywords_reversed from the Fiorentine Minchiate
deck to corresponding Earthman cards:
• Major: explicit number-to-number map based on card correspondences.
• Minor/Middle: same number, suit mapped (BRANDS→WANDS, GRAILS→CUPS,
BLADES→SWORDS, CROWNS→PENTACLES). Cards with no Fiorentine counterpart
stay with empty keyword lists.
"""
from django.db import migrations
# ── 1. Suit rename map ────────────────────────────────────────────────────────
SUIT_RENAMES = {
"WANDS": "BRANDS",
"CUPS": "GRAILS",
"SWORDS": "BLADES",
}
# ── 2. Major arcana: Earthman number → Fiorentine number ─────────────────────
# Cards without a Fiorentine counterpart are omitted (keywords stay empty).
MAJOR_KEYWORD_MAP = {
0: 0, # The Schiz → The Fool
1: 1, # Pope I (President) → The Magician
2: 2, # Pope II (Tsar) → The High Priestess
3: 3, # Pope III (Chairman) → The Empress
4: 4, # Pope IV (Emperor) → The Emperor
5: 5, # Pope V (Chancellor) → The Hierophant
6: 8, # Virtue VI (Controlled Folly) → Strength
7: 11, # Virtue VII (Not-Doing) → Justice
8: 14, # Virtue VIII (Losing Self-Importance) → Temperance
# 9: Prudence — no Fiorentine equivalent
10: 10, # Wheel of Fortune → Wheel of Fortune
11: 7, # The Junkboat → The Chariot
12: 12, # The Junkman → The Hanged Man
13: 13, # Death → Death
14: 15, # The Traitor → The Devil
15: 16, # Disco Inferno → The Tower
# 16: Torre Terrestre (Purgatory) — no equivalent
# 17: Fantasia Celestia (Paradise) — no equivalent
18: 6, # Virtue XVIII (Stalking) → The Lovers
# 19: Virtue XIX (Intent / Hope) — no equivalent
# 20: Virtue XX (Dreaming / Faith)— no equivalent
# 2138: Classical Elements + Zodiac — no equivalents
39: 17, # Wanderer XXXIX (Polestar) → The Star
40: 18, # Wanderer XL (Antichthon) → The Moon
41: 19, # Wanderer XLI (Corestar) → The Sun
# 4249: Planets + The Binary — no equivalents
50: 20, # The Eagle → Judgement
51: 21, # Divine Calculus → The World
}
# ── 3. Minor suit map: Earthman (post-rename) → Fiorentine ───────────────────
MINOR_SUIT_MAP = {
"BRANDS": "WANDS",
"GRAILS": "CUPS",
"BLADES": "SWORDS",
"CROWNS": "PENTACLES",
}
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
except DeckVariant.DoesNotExist:
return # decks not seeded — nothing to do
# ── Step 1: rename Earthman suit codes + card names ───────────────────────
for old_suit, new_suit in SUIT_RENAMES.items():
old_display = old_suit.capitalize() # e.g. "Wands"
new_display = new_suit.capitalize() # e.g. "Brands"
cards = TarotCard.objects.filter(deck_variant=earthman, suit=old_suit)
for card in cards:
card.name = card.name.replace(f" of {old_display}", f" of {new_display}")
card.suit = new_suit
card.save()
# ── Step 2: copy major arcana keywords ───────────────────────────────────
fio_major = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, arcana="MAJOR")
}
for em_num, fio_num in MAJOR_KEYWORD_MAP.items():
fio_card = fio_major.get(fio_num)
if not fio_card:
continue
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=em_num
).update(
keywords_upright=fio_card.keywords_upright,
keywords_reversed=fio_card.keywords_reversed,
)
# ── Step 3: copy minor/middle arcana keywords ─────────────────────────────
for em_suit, fio_suit in MINOR_SUIT_MAP.items():
fio_by_number = {
card.number: card
for card in TarotCard.objects.filter(deck_variant=fiorentine, suit=fio_suit)
}
for em_card in TarotCard.objects.filter(deck_variant=earthman, suit=em_suit):
fio_card = fio_by_number.get(em_card.number)
if fio_card:
em_card.keywords_upright = fio_card.keywords_upright
em_card.keywords_reversed = fio_card.keywords_reversed
em_card.save()
def reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
# Reverse suit renames
reverse_renames = {new: old for old, new in SUIT_RENAMES.items()}
for new_suit, old_suit in reverse_renames.items():
new_display = new_suit.capitalize()
old_display = old_suit.capitalize()
cards = TarotCard.objects.filter(deck_variant=earthman, suit=new_suit)
for card in cards:
card.name = card.name.replace(f" of {new_display}", f" of {old_display}")
card.suit = old_suit
card.save()
# Clear all Earthman keywords
TarotCard.objects.filter(deck_variant=earthman).update(
keywords_upright=[],
keywords_reversed=[],
)
class Migration(migrations.Migration):
dependencies = [
("epic", "0025_earthman_middle_arcana_and_major_icons"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -0,0 +1,65 @@
"""
Schema + data migration:
1. Add `cautions` JSONField (list, default=[]) to TarotCard.
2. Seed The Schizo (Earthman MAJOR #1) with 4 rival-interaction cautions.
All other cards default to [] — the UI shows a placeholder when empty.
"""
from django.db import migrations, models
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">The Pervert</span> when it'
' comes under dominion of <span class="card-ref">The Occultist</span>, which in turn'
' reverses into <span class="card-ref">Pestilence</span>.',
'This card will reverse into <span class="card-ref">The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">The Despot</span>, which in turn'
' reverses into <span class="card-ref">War</span>.',
'This card will reverse into <span class="card-ref">The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">Famine</span>.',
'This card will reverse into <span class="card-ref">The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">The Fascist</span>, which in turn'
' reverses into <span class="card-ref">Death</span>.',
]
def seed_schizo_cautions(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=SCHIZO_CAUTIONS)
def clear_schizo_cautions(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0026_earthman_suit_renames_and_keywords"),
]
operations = [
migrations.AddField(
model_name="tarotcard",
name="cautions",
field=models.JSONField(default=list),
),
migrations.RunPython(seed_schizo_cautions, reverse_code=clear_schizo_cautions),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2026-04-07 03:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0027_tarotcard_cautions'),
]
operations = [
migrations.AlterField(
model_name='tarotcard',
name='suit',
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('CROWNS', 'Crowns'), ('BRANDS', 'Brands'), ('GRAILS', 'Grails'), ('BLADES', 'Blades')], max_length=10, null=True),
),
]

View File

@@ -0,0 +1,61 @@
"""
Data fix: clear Schizo cautions from The Nomad (number=0) if present,
and ensure they land on The Schizo (number=1).
"""
from django.db import migrations
SCHIZO_CAUTIONS = [
'This card will reverse into <span class="card-ref">I. The Pervert</span> when it'
' comes under dominion of <span class="card-ref">II. The Occultist</span>, which in turn'
' reverses into <span class="card-ref">II. Pestilence</span>.',
'This card will reverse into <span class="card-ref">I. The Paranoiac</span> when it'
' comes under dominion of <span class="card-ref">III. The Despot</span>, which in turn'
' reverses into <span class="card-ref">III. War</span>.',
'This card will reverse into <span class="card-ref">I. The Neurotic</span> when it'
' comes under dominion of <span class="card-ref">IV. The Capitalist</span>, which in turn'
' reverses into <span class="card-ref">IV. Famine</span>.',
'This card will reverse into <span class="card-ref">I. The Suicidal</span> when it'
' comes under dominion of <span class="card-ref">V. The Fascist</span>, which in turn'
' reverses into <span class="card-ref">V. Death</span>.',
]
def forward(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=0
).update(cautions=[])
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=SCHIZO_CAUTIONS)
def reverse(apps, schema_editor):
TarotCard = apps.get_model("epic", "TarotCard")
DeckVariant = apps.get_model("epic", "DeckVariant")
try:
earthman = DeckVariant.objects.get(slug="earthman")
except DeckVariant.DoesNotExist:
return
TarotCard.objects.filter(
deck_variant=earthman, arcana="MAJOR", number=1
).update(cautions=[])
class Migration(migrations.Migration):
dependencies = [
("epic", "0028_alter_tarotcard_suit"),
]
operations = [
migrations.RunPython(forward, reverse_code=reverse),
]

View File

@@ -0,0 +1,23 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epic', '0029_fix_schizo_cautions'),
]
operations = [
migrations.AddField(
model_name='sigreservation',
name='seat',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='sig_reservation',
to='epic.tableseat',
),
),
]

View File

@@ -216,13 +216,19 @@ class TarotCard(models.Model):
CUPS = "CUPS" CUPS = "CUPS"
SWORDS = "SWORDS" SWORDS = "SWORDS"
PENTACLES = "PENTACLES" # Fiorentine 4th suit PENTACLES = "PENTACLES" # Fiorentine 4th suit
CROWNS = "CROWNS" # Earthman 4th suit (renamed from Pentacles) CROWNS = "CROWNS" # Earthman 4th suit
BRANDS = "BRANDS" # Earthman Wands
GRAILS = "GRAILS" # Earthman Cups
BLADES = "BLADES" # Earthman Swords
SUIT_CHOICES = [ SUIT_CHOICES = [
(WANDS, "Wands"), (WANDS, "Wands"),
(CUPS, "Cups"), (CUPS, "Cups"),
(SWORDS, "Swords"), (SWORDS, "Swords"),
(PENTACLES, "Pentacles"), (PENTACLES, "Pentacles"),
(CROWNS, "Crowns"), (CROWNS, "Crowns"),
(BRANDS, "Brands"),
(GRAILS, "Grails"),
(BLADES, "Blades"),
] ]
deck_variant = models.ForeignKey( deck_variant = models.ForeignKey(
@@ -239,6 +245,7 @@ class TarotCard(models.Model):
group = models.CharField(max_length=100, blank=True) # Earthman major grouping group = models.CharField(max_length=100, blank=True) # Earthman major grouping
keywords_upright = models.JSONField(default=list) keywords_upright = models.JSONField(default=list)
keywords_reversed = models.JSONField(default=list) keywords_reversed = models.JSONField(default=list)
cautions = models.JSONField(default=list)
class Meta: class Meta:
ordering = ["deck_variant", "arcana", "suit", "number"] ordering = ["deck_variant", "arcana", "suit", "number"]
@@ -290,8 +297,16 @@ class TarotCard(models.Model):
self.SWORDS: 'fa-gun', self.SWORDS: 'fa-gun',
self.PENTACLES: 'fa-star', self.PENTACLES: 'fa-star',
self.CROWNS: 'fa-crown', self.CROWNS: 'fa-crown',
self.BRANDS: 'fa-wand-sparkles',
self.GRAILS: 'fa-trophy',
self.BLADES: 'fa-gun',
}.get(self.suit, '') }.get(self.suit, '')
@property
def cautions_json(self):
import json
return json.dumps(self.cautions)
def __str__(self): def __str__(self):
return self.name return self.name
@@ -344,6 +359,10 @@ class SigReservation(models.Model):
gamer = models.ForeignKey( gamer = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations' settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations'
) )
seat = models.ForeignKey(
'TableSeat', null=True, blank=True,
on_delete=models.SET_NULL, related_name='sig_reservation',
)
card = models.ForeignKey( card = models.ForeignKey(
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations' 'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations'
) )
@@ -369,9 +388,9 @@ class SigReservation(models.Model):
def sig_deck_cards(room): def sig_deck_cards(room):
"""Return 36 TarotCard objects forming the Significator deck (18 unique × 2). """Return 36 TarotCard objects forming the Significator deck (18 unique × 2).
PC/BC pair → WANDS + CROWNS Middle Arcana court cards (1114): 8 unique PC/BC pair → BRANDS/WANDS + CROWNS Middle Arcana court cards (1114): 8 unique
SC/AC pair → SWORDS + CUPS Middle Arcana court cards (1114): 8 unique SC/AC pair → BLADES/SWORDS + GRAILS/CUPS Middle Arcana court cards (1114): 8 unique
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
Total: 18 unique × 2 (levity + gravity piles) = 36 cards. Total: 18 unique × 2 (levity + gravity piles) = 36 cards.
""" """
deck_variant = room.owner.equipped_deck deck_variant = room.owner.equipped_deck
@@ -380,13 +399,13 @@ def sig_deck_cards(room):
wands_crowns = list(TarotCard.objects.filter( wands_crowns = list(TarotCard.objects.filter(
deck_variant=deck_variant, deck_variant=deck_variant,
arcana=TarotCard.MIDDLE, arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.WANDS, TarotCard.CROWNS], suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
number__in=[11, 12, 13, 14], number__in=[11, 12, 13, 14],
)) ))
swords_cups = list(TarotCard.objects.filter( swords_cups = list(TarotCard.objects.filter(
deck_variant=deck_variant, deck_variant=deck_variant,
arcana=TarotCard.MIDDLE, arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.SWORDS, TarotCard.CUPS], suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
number__in=[11, 12, 13, 14], number__in=[11, 12, 13, 14],
)) ))
major = list(TarotCard.objects.filter( major = list(TarotCard.objects.filter(
@@ -406,13 +425,13 @@ def _sig_unique_cards(room):
wands_crowns = list(TarotCard.objects.filter( wands_crowns = list(TarotCard.objects.filter(
deck_variant=deck_variant, deck_variant=deck_variant,
arcana=TarotCard.MIDDLE, arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.WANDS, TarotCard.CROWNS], suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
number__in=[11, 12, 13, 14], number__in=[11, 12, 13, 14],
)) ))
swords_cups = list(TarotCard.objects.filter( swords_cups = list(TarotCard.objects.filter(
deck_variant=deck_variant, deck_variant=deck_variant,
arcana=TarotCard.MIDDLE, arcana=TarotCard.MIDDLE,
suit__in=[TarotCard.SWORDS, TarotCard.CUPS], suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
number__in=[11, 12, 13, 14], number__in=[11, 12, 13, 14],
)) ))
major = list(TarotCard.objects.filter( major = list(TarotCard.objects.filter(

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #354a9c;
}
.cls-2 {
fill: #381507;
}
.cls-3 {
stroke-width: 2.75px;
}
.cls-3, .cls-4 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
}
.cls-5 {
fill: #4f66d4;
}
.cls-6 {
fill: #4258b8;
}
.cls-7 {
fill: #3d180d;
}
.cls-8 {
fill: #3a1709;
}
.cls-4 {
stroke-width: 2.2px;
}
</style>
</defs>
<path class="cls-5" d="M185.31,203.37l.12.54,1.16,5.4-2.22.49c-.08,8.17-.62,15.76-1.54,24.08-.26,2.33.18,7.31,3.04,7.58,4.78.44,9.33-1.88,14.1-2.1,1.59-.07,3.44-.05,4.69-.32l.98,9.54c.24,2.25,2.12,2.5,3.74,3.16,1.99.81,2.31,3.55,3.46,5.16l3.75,5.23c.62.86,1.45,1.66,2.51,1.36-.12,2.28.15,4.61.11,7.14l-.44,1.49c-1.47-.84-2.37,1.63-3.55,1.77l-16.08,1.97-21.66,2.39c4.99,1.47,9.61,1.45,14.55,2.03l23.5,2.78c.22,1.66.49.84,1.47.31,1.01,6.14,2.22,12.45,2.53,19.3,1.2,4.17.5,8.59-3.88,10.35l-3.91,2.35c.65.86,1.46,1.29,1.81,2.27,2.77,1.35,3.4,4.57,3.09,7.75,2.44,5.7,2.42,11.51,1.87,17.57l-.91,9.96c-.14,1.55-1.69,1.51-2.77,1.41l-17.57-1.6c-4.26-.39-8.71.46-12.16-2.84l-5.53-7.04c-.25.2-.35.47-.36.58,0-.11.11-.38.36-.58-.25.2-.35.47-.36.58-.28,3.07-.81,5.92-2.85,8.97l-21.21,1.95c-.12-1.76-.26-3.48-1.01-5.15l-4.42-9.91-3.75-25.3c-.14-.97-.63-1.8-1.07-2.16.43-1.73-.03-4.16-.58-6.18-1.9-4.98-3.59-9.83-4.83-15.35.18-.51-1.33-1.69-.39-2.35l1.62-1.16c1-.71-.63-1.33-.17-2.07.4-.66,1.75-.46,1.97-1.2.33-1.08-.35-2.18-1.64-2.16l-4.2.07-9.67-35.31c-.2-.75-.95-.91-1.34-1.05-.54-.19-1.5,0-1.53.93-.02.66-.64,1.72-1.09,2.78-1.2-1.34-4.74-2.35-5.46-.22l-12.57,37.25-7.39,1.96-1.8.57c-.58.18-.63,2.12,0,2.14,1.81.06,3.73-1.13,5.06.44-1.62.37-4.59.86-3.54,2.77.86,1.55,3.62.48,4.95.31l-4.78,15.62c-.24.78.03,1.76.57,2.34.4.43,1.33.72,2.04-.13,1.08-1.29,1.25-3.2,1.82-4.45.97-.38,2.22.37.73.97-.13,1.21-.23,2.19.18,3.14.23.53.84.92,2.01.61,2.71-6.4,4.7-12.72,6.8-19.73l18.11-2.9,5.96,21.86c.44,1.61,1.47,1.6,2.91,1.32,1.12-.22.51-1.69.51-2.99v-1.31c0-.37.34-.39,1.2-.31-1.04,1.12-.03,2.08.5,3.48.36.96,1.5,1.25,2.74,1.23l-.5,20.05c-.09,3.58-.56,6.95-.22,10.34l-21.98.14-2.92-9.49c2.68-4.69,5.77-9.41,3.04-13.61l-6.91,1.61c-1.59.37.86,2.08.68,2.8l-1.98,7.57c-.27,1.01.11,2.26-.46,3.03-1.37,1.88-6.3,1.61-9.49.15-1.3-2.98,1.24-7.91.16-13.45-.44-2.25-3.52-1.29-5.21-1.15l-8.08.67-6.35-5.57c.37-4.82-6.34-6.88-5.95-12.46.07-.96,1.36-1.31,1.96-2.42l5.08-9.34c.18-1.24-1.67-1.79-2.25-2.13l-3.45-1.97c-1-.57-1.54-1.46-1.61-2.3-.11-1.23,3.32-3.74,6.12-4.08l23.77-2.92c-4.84-1.4-9.6-.46-14.47-.77l-20.58-1.25-1.75-17.45c-.27-2.73-1.52-5.99.31-8.64,1.48-2.15,4.9-2.45,7.04-3.85,3.01-1.97-4.02-5.01-3.67-6.6,1.58-7.04-3.55-9.14-2.92-17.11l1.35-16.91c19.69,3.11,39.13,3.08,58.78.65,2.37-.29,3.87.88,5.27,2.25l.56.54-.56-.54-2.23,1.33c3.23,4.36,4.37,8.99,5.14,13.85l2.71,17.1,3.13,17.22c.82-.18.95-1.1.85-1.6-.94-4.55-.24-9.14-.6-13.84-.32-4.1-.56-7.97.06-12.11.92-6.26,1.04-12.4-.43-18.5-.18-.76-1.4-.95-1.7-.74.48-1.08.49-3.87,2.16-4.43l15.4-1.26,24.35-2.21ZM187.63,257.75c.4.93.52,1.9,1.14,2.91,2.36,3.88,4.35-8.06-7.31-13.58-7.5-3.55-15.29-1.54-21.1,4.83-13.04,14.31-17.28,39.69-4.52,53.8,5.7,6.31,14.78,8.03,22.8,5.87,13.32-3.6,20.27-20.17,13.32-16.11-.83,1.08-1.92.91-1.28-.45.23-1.19-.5-2.25-1.19-2.44-2.65-.75-3.54,8.1-13.46,10.47-5.92,1.42-11.86-1.51-14.55-6.83-4.2-8.32-4.34-17.68-1.05-26.64,3.45-9.39,10.35-17.68,18.54-13.94,4.41,2.01,4.83,8.81,5.68,8.88s1.2-.15,1.64-.32c.27-.1,1.75-2.67.35-6.47.34.07.6.1.97.03Z"/>
<path class="cls-6" d="M142.89,343.17l.29,2.88c.86.07,1.58.05,2.04.23.67.25,1.91,1.3,1.32,2.05-1.46,1.19-1.67,3.08-2.56,5.44l-38.54,2.82c-2.19.16-4,.22-5.07-1.87-.06.09-.12.09-.17.04l-31.82-30.7.34-.51,6.2,1.52c6.89,2.24,14.28,1.74,21.71,1.33,1.56-.09,1.37,2.99,1.43,3.85-.3,7.59-.1,14.62,2.15,21.47.05.41.41.65.83.61.39-.4.95-.17.83-.59l-.37-1.33.4-13.65c.01-.34.23-.76.19-.97-.05-.23.41-.49.79-.42,3.19,1.46,8.11,1.72,9.49-.15.56-.77.19-2.01.46-3.03l1.98-7.57c.19-.72-2.26-2.43-.68-2.8l6.91-1.61c2.74,4.2-.36,8.92-3.04,13.61l2.92,9.49,21.98-.14Z"/>
<path class="cls-6" d="M89.74,321.43c-1.43.12-3.07.88-4.9.94l-9.56.29c-.16,1.21.7,1.9-.37,2.41l-6.2-1.52-.57-.13-.57-.14c.06-.16.14-.32.15-.48l1.45-15.57,1.33-17.08,1.52-13.95,20.58,1.25c4.86.31,9.63-.63,14.47.77l-23.77,2.92c-2.8.34-6.23,2.85-6.12,4.08.07.84.61,1.73,1.61,2.3l3.45,1.97c.58.33,2.43.89,2.25,2.13l-5.08,9.34c-.6,1.11-1.89,1.46-1.96,2.42-.4,5.59,6.32,7.64,5.95,12.46l6.35,5.57Z"/>
<path class="cls-5" d="M185.31,203.37l.41.36,32.83,32.44c.15.15.23.32.17.52l-3.36-.08c-.18,0-.37-.17-.52.11-9.2-3.04-24.62.27-26.12-.81-2.61-1.89,1.62-13.42-2.11-26.59l-1.16-5.4-.12-.54Z"/>
<path class="cls-6" d="M219.09,263.48c-1.07.3-1.89-.5-2.51-1.36l-3.75-5.23c-1.15-1.61-1.47-4.34-3.46-5.16-1.62-.66-3.5-.91-3.74-3.16l-.98-9.54c2.56-.56,5.24-.75,8.24-.37l1.71-.29c.49-.08,1.13-1.43.74-1.77-.18,0-.37-.17-.52.11.15-.28.34-.11.52-.11l3.36.08c.06-.2-.01-.36-.17-.52.15.15.23.32.17.52,2.85,1.17,1.38,7.41,1.07,13.48l-.68,13.31Z"/>
<path class="cls-6" d="M137.02,209.09l5.09,4.92c1.03-.68.94-1.93,1.29-2.74.3-.21,1.51-.02,1.7.74,1.47,6.1,1.35,12.23.43,18.5-.61,4.14-.37,8.01-.06,12.11.37,4.7-.33,9.3.6,13.84.1.5-.03,1.42-.85,1.6l-3.13-17.22-2.71-17.1c-.77-4.87-1.91-9.49-5.14-13.85l2.23-1.33.56.54Z"/>
<path class="cls-1" d="M155.13,354.34l-6.55-5.25c-.5-.4-.58-1.25-1.2-1.09-.28.07-.6.17-.85.32.59-.75-.65-1.8-1.32-2.05-.47-.17-1.18-.15-2.04-.23l-.29-2.88c-.34-3.39.13-6.76.22-10.34l.5-20.05c.35-.26.61-.87,1.26-.95.44.36.93,1.18,1.07,2.16l3.75,25.3,4.42,9.91c.75,1.67.89,3.39,1.01,5.15Z"/>
<path class="cls-1" d="M218.76,272.12c.55,2.96-1.28,5.06-3.13,7.88-.92,1.4,1.16,2.13,1.36,3.36-.98.53-1.25,1.36-1.47-.31l-23.5-2.78c-4.93-.58-9.56-.56-14.55-2.03l21.66-2.39,16.08-1.97c1.17-.14,2.07-2.61,3.55-1.77Z"/>
<path class="cls-8" d="M144.88,311.82c-.66.08-.92.69-1.26.95-1.24.01-2.37-.28-2.74-1.23-.53-1.4-1.54-2.36-.5-3.48-.86-.08-1.19-.06-1.2.31v1.31c0,1.3.6,2.76-.52,2.99-1.43.29-2.47.29-2.91-1.32l-5.96-21.86-18.11,2.9c-2.1,7.01-4.09,13.33-6.8,19.73-1.17.31-1.78-.08-2.01-.61-.41-.95-.31-1.94-.18-3.14,1.5-.6.24-1.35-.73-.97-.56,1.24-.74,3.16-1.82,4.45-.71.85-1.64.57-2.04.13-.54-.58-.8-1.56-.57-2.34l4.78-15.62c-1.33.17-4.1,1.25-4.95-.31-1.05-1.92,1.92-2.4,3.54-2.77-1.34-1.57-3.26-.38-5.06-.44-.62-.02-.58-1.96,0-2.14l1.8-.57,7.39-1.96,12.57-37.25c.72-2.13,4.25-1.12,5.46.22.45-1.06,1.07-2.12,1.09-2.78.03-.94.98-1.12,1.53-.93.39.13,1.13.3,1.34,1.05l9.67,35.31,4.2-.07c1.29-.02,1.98,1.07,1.64,2.16-.23.73-1.57.54-1.97,1.2-.45.74,1.17,1.36.17,2.07l-1.62,1.16c-.93.67.57,1.85.39,2.35,1.24,5.52,2.93,10.37,4.83,15.35.55,2.02,1.01,4.45.58,6.18ZM122.28,263.01l-7.68,21.2,13.37-1.5-5.69-19.69Z"/>
<path class="cls-7" d="M187.63,257.75l-1.21-2.81c-.33-.78-.83-2.11-2.39-2.23l-.53-1.37c-.16-.41-.82-.21-1.64-.58-1.01-.56-1.29.55.06.58,2.16,2.07,3.86,4.01,4.73,6.38,1.4,3.8-.08,6.37-.35,6.47-.45.17-.81.39-1.64.32s-1.27-6.87-5.68-8.88c-8.19-3.73-15.09,4.55-18.54,13.94-3.29,8.97-3.16,18.32,1.05,26.64,2.69,5.33,8.63,8.25,14.55,6.83,9.92-2.37,10.81-11.22,13.46-10.47.69.19,1.42,1.25,1.19,2.44-.64,1.37.45,1.54,1.28.45,6.95-4.06,0,12.51-13.32,16.11-8.02,2.17-17.1.44-22.8-5.87-12.75-14.1-8.52-39.49,4.52-53.8,5.8-6.37,13.59-8.37,21.1-4.83,11.66,5.51,9.67,17.45,7.31,13.58-.61-1.01-.74-1.98-1.14-2.91ZM157.46,302.61l3.6,3.77c.64.67.95.15,1.82.3.52.09-.23-.61-.29-.93-.07-.41-1.13-.1-1.37-.35-1.17-1.26-1.91-3.37-3.77-2.78Z"/>
<path class="cls-1" d="M186.59,209.31c3.74,13.17-.49,24.7,2.11,26.59,1.5,1.08,16.92-2.22,26.12.81.15-.28.34-.11.52-.11.4.33-.25,1.68-.74,1.77l-1.71.29c-3-.38-5.68-.19-8.24.37-1.25.27-3.1.25-4.69.32-4.76.22-9.32,2.54-14.1,2.1-2.86-.26-3.3-5.24-3.04-7.58.92-8.32,1.46-15.9,1.54-24.08l2.22-.49Z"/>
<path class="cls-1" d="M102.87,335.37c-.38-.07-.84.19-.79.42.05.21-.18.63-.19.97l-.4,13.65.37,1.33c.12.42-.44.19-.83.59-.42.03-.78-.2-.83-.61-2.25-6.85-2.45-13.87-2.15-21.47-.06-.86.13-3.93-1.43-3.85-7.43.41-14.82.91-21.71-1.33,1.07-.51.21-1.2.37-2.41l9.56-.29c1.83-.06,3.48-.82,4.9-.94l8.08-.67c1.68-.14,4.77-1.1,5.21,1.15,1.08,5.55-1.46,10.47-.16,13.45Z"/>
<path class="cls-1" d="M187.63,257.75c-.37.07-.64.04-.97-.03-.88-2.38-2.57-4.31-4.73-6.38-1.35-.04-1.07-1.15-.06-.58.82.37,1.48.17,1.64.58l.53,1.37c1.56.12,2.06,1.45,2.39,2.23l1.21,2.81Z"/>
<polygon class="cls-5" points="122.28 263.01 127.97 282.71 114.6 284.21 122.28 263.01"/>
<path class="cls-1" d="M157.46,302.61c1.86-.59,2.59,1.52,3.77,2.78.23.25,1.3-.06,1.37.35.05.32.81,1.02.29.93-.87-.15-1.18.38-1.82-.3l-3.6-3.77Z"/>
<path class="cls-2" d="M210.46,277.3l6.02,1.81c1.38.43.78,2.5-.62,2.11,0,0-6.03-1.81-6.03-1.81-1.97-.86-4.17-1.86-6.07-2.91,1.33.16,5.03.6,6.7.81h0Z"/>
<path class="cls-4" d="M185.31,203.37l-24.35,2.21-15.4,1.26c-1.66.57-1.68,3.35-2.16,4.43-.36.81-.27,2.06-1.29,2.74l-5.09-4.92-.56-.54c-1.41-1.37-2.9-2.54-5.27-2.25-19.66,2.42-39.1,2.45-58.78-.65l-1.35,16.91c-.64,7.97,4.5,10.07,2.92,17.11-.36,1.59,6.68,4.62,3.67,6.6-2.14,1.4-5.56,1.7-7.04,3.85-1.83,2.66-.58,5.92-.31,8.64l1.75,17.45-1.52,13.95-1.33,17.08-1.45,15.57-.15.48"/>
<path class="cls-4" d="M185.32,203.34l.4.39,32.83,32.44c.15.15.23.32.17.52l-3.36-.08c-.18,0-.37-.17-.52.11-9.2-3.04-24.62.27-26.12-.81-2.61-1.89,1.62-13.42-2.11-26.59l-1.16-5.4-.11-.58Z"/>
<path class="cls-4" d="M213.54,317.63c2.77,1.35,3.4,4.57,3.09,7.75,2.44,5.7,2.42,11.51,1.87,17.57l-.91,9.96c-.14,1.55-1.69,1.51-2.77,1.41l-17.57-1.6c-4.26-.39-8.71.46-12.16-2.84l-5.53-7.04c-.25.2-.35.47-.36.58-.28,3.07-.81,5.92-2.85,8.97l-21.21,1.95-6.55-5.25c-.5-.4-.58-1.25-1.2-1.09-.28.07-.6.17-.85.32-1.46,1.19-1.67,3.08-2.56,5.44l-38.54,2.82c-2.19.16-4,.22-5.07-1.87-.62-.8-.65-1.96-.17-3.01-2.25-6.85-2.45-13.87-2.15-21.47-.06-.86.13-3.93-1.43-3.85-7.43.41-14.82.91-21.71-1.33l-6.2-1.52-.57-.13-.57-.14"/>
<polyline class="cls-3" points="100.19 354.76 68.38 324.06 67.57 323.28"/>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #39170a;
}
.cls-2 {
fill: #6b1f65;
}
.cls-3 {
fill: #852f7e;
}
.cls-4 {
fill: #3d1a0d;
}
.cls-5 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2px;
}
.cls-6 {
fill: #9e3d96;
}
</style>
</defs>
<path class="cls-6" d="M134.56,198.73c-.18.4-.63,1.06-.86,1.01l-1.53-.32,2.73,6.11c3.16,4.21,3.45,9.15,4.36,14.26l5.43,30.42-.31-26.42.69-6.77-.39-15.14c.78.13,1.2.48,1.86.21.09,1.59-.38,3.25-.01,4.91,3.95,1.89,3.31,4.73,5.82,4.74,1.17,0,3.35.51,3.8-.95.88-2.85,3.04-3.98,3.96-8.34,2.01-.62,4.59.12,5.2,2.41l2.94,4.65.47,2.59c.11.63,3.07.5,5.91,4.7.74-2.1,2.63-2.9,4.18-3.47,2.1-.76,3.81.92,5.23,2.46l1.28.36-1.2,10.28c-.16,1.36.06,4.75,1.34,5.26l12.32-1.33c1.3-.14,2.73-.73,4.27-.46-.74,2.06,2.02,3.53,2.3,4.95.63,3.15.89,5.85,3.93,7.95,1.18.81,2.14,2.65,4.4,1.82l2.14,4.34c-1.97,3.85-2.69,7.52-1.18,11.55l-7.95,3.92-9.59,1.55-24.02,4.56,31.72.18c3.86-1.21,7.59-.06,10.11,3.3-3.28,1.87-2.97,4.67-2,7.19,3.17,8.23-.69,15.5-7.22,20.39-2.27,2.33-3.26,5.96-4.66,8.69l-14.97-1.08-1.98.67c-1.17.4-1.4,6.07-1.2,10.28l-5.97.64c.01,1.31.89,2.27,1.35,3.52l-3.55,5.37c-5.53-.62-8.28,7-13.89,10.9-1.98,1.37-3.73-.25-5.2-.67-1.79-.51-3.25-.83-4.12-2.65-1.17.63-2.28.31-3.09,0-.93-.36-1.45-1.42-1.58-2.63l-1.24-12.07-.54-21.41-3.37,26.93-2.32,11.58c-1.35-.6-2.56-.79-3.93-.64-2.87.32-5.45-.38-8.31-.41-1.55-.02-1.88-1.53-3.11-3.07l2.02-2.29c-1.1-2.21-3.48-2.22-5.28-3.2-.75-.41-1.27-1.25-2.14-1.2-2.2.13.67,6.44-2.89,6.37l-6.18-.11c-.97-.02-1.52-.97-1.46-1.38l.36-2.39c-2.16-3.31-6.64-4.97-10.34-6.05l-4.61-1.35,9.71-11.1c1.03-1.17,2.56-2.27,2.2-3.91l-10.97,8.35c-4.57-1.25-8.28-3.55-8.21-8.02-.63-.81-1.9-1.86-1.64-2.9.76-2.95,3.93-2.39,4-6.01.03-1.5-1.05-2.01-2.21-3.07-1.5-1.37-2.59-3.79-4.86-4.28-1.89-.41-6.44-4.24-7.02-6.09-1.01-3.25,2.85-5.63.92-8.2l8.46-2.25,18.21-3.46.55,22.34-4.07-1.09c-.62-.17-1.15.71-1.26,1.11-.71,2.66,6.57,4.49,6.46,4.96-.06.26-.4.99-.55.77-.26-.37-.68-.81-1.29-.89l-2.57-.33c-.63-.08-1.82,2.04-.78,2.4,13.51,4.72,30.52,4.52,40.19-6.04,4.26-4.65,7-10.96,5.44-17.11-1.88-7.43-8.07-12.69-15.99-13.65,4.61-3.97,8.07-9.52,6.06-15.48s-7.82-9.61-13.78-10.33c-13.8-1.66-26.43,7.86-23.9,11.48l1.82.27c.83.77.9,1.54,1.58,1.28l2.21-.86-.14,17.59-10.69.46-11.02-.53c-3.1-.15-5.69-2.07-8.52-1.76l-2.44-19.92c-1.31-2.93-.76-6.46,2.46-7.96,3.42-1.59,4.59-3.37,5.99-2.92.03-2.33-2.37-3.38-4.36-4.69l-.32-6.53c-5.46-9.7-.92-17.59-1.51-28.43l16.23,2.03,23.56-.02,16.48-1.23c2.97-.22,5.24-1.39,7.61.8ZM185.54,293.84c2.42-3.82,5.39-7.88,3.02-9.23s-3.38,6.71-11.7,9.93c-4.18,1.62-8.99.92-12.68-1.81-3.38-2.5-5.13-6.55-6.15-11.09-3.58-15.97,6.89-36.59,18.87-32.85,6.82,2.13,4.36,12.37,8.6,8.12.92-1.52.02-3.6.33-5.64l2.51,3.38c1.54-.91,1.02-2.87.63-4.33-1.61-6.12-7.34-9.87-13.12-11.04-6.3-1.28-11.96,1.46-16.42,6.15-6.23,6.56-9.49,15.21-11.29,23.96-2.95,14.33,3.3,32.61,19.18,34.63,11.19,1.43,22.11-4.68,26.13-15.27.19-.49.04-1.09.04-1.41,0-.73-2.81-.21-2.68-.3-1.54,1.13-1.96,6.72-5.28,6.8Z"/>
<path class="cls-3" d="M74.46,278.72c1.93,2.57-1.93,4.94-.92,8.2.57,1.85,5.13,5.69,7.02,6.09,2.27.49,3.37,2.91,4.86,4.28,1.15,1.06,2.24,1.57,2.21,3.07-.07,3.62-3.24,3.06-4,6.01-.26,1.03,1.01,2.08,1.64,2.9-.07,4.47,3.65,6.77,8.21,8.02l10.97-8.35c.36,1.64-1.18,2.73-2.2,3.91l-9.71,11.1,4.61,1.35c3.7,1.08,8.18,2.75,10.34,6.05l-.36,2.39c-.06.4.49,1.36,1.46,1.38l6.18.11c3.56.07.69-6.24,2.89-6.37.86-.05,1.39.79,2.14,1.2,1.8.99,4.18.99,5.28,3.2l-2.02,2.29c1.23,1.55,1.56,3.06,3.11,3.07,2.87.03,5.44.73,8.31.41,1.37-.15,2.57.04,3.93.64l2.32-11.58,3.37-26.93.54,21.41,1.24,12.07c.12,1.21.65,2.27,1.58,2.63.81.32,1.92.64,3.09,0,.86,1.82,2.33,2.14,4.12,2.65,1.47.42,3.22,2.04,5.2.67,5.6-3.9,8.36-11.52,13.89-10.9l3.55-5.37c-.47-1.25-1.34-2.21-1.35-3.52l5.97-.64.85,18.06-.17,3.08c.67-.03,1.46-.09,2.22.11l-1.57,4.51-.4,1.16-.74-.15-9.48-.44-13.17-.04c-5.51-.02-10.81.2-15.91-2.37-4.17,3.3-8.82,3.85-13.75,4.04l-11.57.44c-4.22.16-8-1.47-12.1-2.44l-9.23-2.18-2.37-2.53-1-1.14c-.19-.22-.42.16-1.49,0l-2.88,3.29c-1.64,3.38-4.27,3.48-7.57,2.93l-8.76-1.47c-1.06-.18-1.68-1.56-2.63-2.24l-.25-10.04c-.09-3.55-2.33-6.92-2.12-10.04l.33-4.87,1.13-9.81c.44-3.77.39-7.85,0-11.53l-.45-4.37c-.88-2.66-.01-5.19,1.15-7.52l3.1-6.18c.48,0,.8.42,1.38.27Z"/>
<path class="cls-3" d="M219.05,227.87l.79.78-.21.25.6.14-.13.88-1.61,30.29-6.75,7.18,6.67,3.15,1.66,35.78c.09,1.99,0,4.05-1.49,5-.19.12-.4.28-.59.29l.1.11-.1-.11-3.17.11c-.29-.45-.37-1.65-1.08-1.62l-5.37.21c-2.72.1-5.69.2-8.32.01,1.39-2.73,2.38-6.36,4.66-8.69,6.53-4.89,10.39-12.17,7.22-20.39-.97-2.52-1.28-5.32,2-7.19-2.53-3.36-6.25-4.51-10.11-3.3l-31.72-.18,24.02-4.56,9.59-1.55,7.95-3.92c-1.51-4.02-.8-7.7,1.18-11.55l-2.14-4.34c-2.26.84-3.21-1-4.4-1.82-3.04-2.1-3.3-4.8-3.93-7.95-.28-1.42-3.04-2.89-2.3-4.95l3.85-.57,8.82.23c.74-.32,1.1.42,1.09-.02-.02-.58.56-1.21.18-1.42l2.43.46.62-.74Z"/>
<path class="cls-3" d="M188.5,197.92c-.14-.14-.26-.35-.42-.36.16.02.28.22.42.36l-.8,2.07.09,1.09-1.96.21c.42,5.17.06,10.03-.5,14.87l-1.28-.36c-1.42-1.53-3.13-3.22-5.23-2.46-1.55.57-3.44,1.37-4.18,3.47-2.84-4.2-5.79-4.07-5.91-4.7l-.47-2.59-2.94-4.65c-.61-2.29-3.19-3.03-5.2-2.41-.92,4.36-3.08,5.48-3.96,8.34-.45,1.46-2.64.96-3.8.95-2.51,0-1.87-2.85-5.82-4.74-.37-1.66.1-3.32.01-4.91-.66.27-1.08-.08-1.86-.21l.39,15.14-.69,6.77.31,26.42-5.43-30.42c-.91-5.11-1.2-10.05-4.36-14.26l-2.73-6.11,1.53.32c.22.05.68-.6.86-1.01,2.07,1.91,4.3,4.1,6.23,6.93l3.69-7.11,37.01-2.68c2.54-1.25,5.05-.41,6.58,1.69.16.02.28.22.42.36Z"/>
<path class="cls-6" d="M217.99,311.6l-.39.42-33.59,33.88-.77.03,1.57-4.51c2.75-7.94,3.11-16.57,1.63-25.15-.21-1.2,1.02-2.43,2.16-2.43l11.01.06c5.33.03,10.37-.9,15.23-2.19l3.17-.11Z"/>
<path class="cls-3" d="M219.05,227.87l-.62.74-2.43-.46c-8.06-2.53-16.09-3.31-24.73-2.08-1.96.28-2.28-4.19-1.56-5.38.71-6.92-.38-13.29-1.92-19.6l-.09-1.09.8-2.07c-.14-.14-.26-.35-.42-.36.16.02.28.22.42.36l30.55,29.94Z"/>
<path class="cls-2" d="M101.08,269.43l.04,3.58-18.21,3.46-8.46,2.25c-.58.15-.9-.27-1.38-.27l2.44-3.67c-.82-2.73-4.33-4.52-4.66-7.18,2.83-.32,5.42,1.61,8.52,1.76l11.02.53,10.69-.46Z"/>
<path class="cls-1" d="M101.08,269.43l.14-17.59-2.21.86c-.67.26-.75-.52-1.58-1.28l-1.82-.27c-2.52-3.62,10.1-13.15,23.9-11.48,5.96.72,11.76,4.34,13.78,10.33s-1.45,11.51-6.06,15.48c7.92.96,14.11,6.22,15.99,13.65,1.56,6.15-1.18,12.46-5.44,17.11-9.67,10.56-26.68,10.76-40.19,6.04-1.04-.36.15-2.48.78-2.4l2.57.33c.61.08,1.03.52,1.29.89.15.22.49-.51.55-.77.11-.47-7.17-2.29-6.46-4.96.11-.41.64-1.28,1.26-1.11l4.07,1.09-.55-22.34-.04-3.58ZM124.8,255.68c1.58-3.1.42-5.63-2.19-7.08-3.93-2.19-8.3-2.63-13.25-1.34v16.49c0,.62.35,1.06.86,1.37.26.15.45-.22,1.12-.47,5.49-1.09,10.87-3.89,13.45-8.97ZM106.69,248.96l-1.41-.03.17,8.81c.23-.03.44-.47.56-.3l.68-8.48ZM129.95,257.42c.65-1.58.8-4.23,1.16-5.86-1.76-.16-.51-1.16-.52-1.51,0-.22-.65-.01-1.42-.09,1.09,3.53.8,7.12-1.99,10.11,1.03.81,1.77-.21,1.99-.75l.77-1.9ZM133.96,284.81c.89-6.65-3.22-11.63-9.46-12.87-5.07-1.01-9.78.28-14.97,1.72l.7,23.53c5.81,1,11.07-.12,16.57-2.3,3.82-1.51,6.61-6.02,7.16-10.08ZM105.85,278.54c-.96,2.12-1.03,4.41.28,6.27.22-1.99.88-3.84-.28-6.27ZM138.89,279.18h-1.19s.2,6.6.2,6.6c.2-.03.41-.45.51-.3.97-1.95.1-4,.47-6.3Z"/>
<path class="cls-4" d="M185.54,293.84c3.32-.08,3.73-5.67,5.28-6.8-.13.1,2.68-.43,2.68.3,0,.32.14.92-.04,1.41-4.02,10.58-14.94,16.69-26.13,15.27-15.88-2.03-22.13-20.3-19.18-34.63,1.8-8.74,5.06-17.4,11.29-23.96,4.45-4.69,10.12-7.43,16.42-6.15,5.78,1.17,11.5,4.93,13.12,11.04.38,1.46.9,3.42-.63,4.33l-2.51-3.38c-.3,2.04.6,4.12-.33,5.64-4.24,4.25-1.78-5.98-8.6-8.12-11.98-3.75-22.45,16.87-18.87,32.85,1.02,4.54,2.77,8.58,6.15,11.09,3.68,2.73,8.49,3.43,12.68,1.81,8.32-3.22,9.32-11.28,11.7-9.93s-.59,5.41-3.02,9.23Z"/>
<path class="cls-2" d="M187.78,201.08c1.54,6.31,2.64,12.68,1.92,19.6-.72,1.2-.4,5.66,1.56,5.38,8.64-1.23,16.67-.45,24.73,2.08.38.2-.2.83-.18,1.42.02.44-.35-.31-1.09.02l-8.82-.23-3.85.57c-1.54-.27-2.98.32-4.27.46l-12.32,1.33c-1.29-.51-1.5-3.9-1.34-5.26l1.2-10.28c.57-4.84.93-9.7.5-14.87l1.96-.21Z"/>
<path class="cls-2" d="M200.05,310.31c2.64.19,5.6.09,8.32-.01l5.37-.21c.71-.03.79,1.17,1.08,1.62-4.86,1.29-9.9,2.22-15.23,2.19l-11.01-.06c-1.13,0-2.36,1.22-2.16,2.43,1.48,8.57,1.13,17.21-1.63,25.15-.76-.2-1.55-.15-2.22-.11l.17-3.08-.85-18.06c-.2-4.21.03-9.88,1.2-10.28l1.98-.67,14.97,1.08Z"/>
<path class="cls-6" d="M133.96,284.81c-.55,4.07-3.34,8.57-7.16,10.08-5.5,2.17-10.76,3.29-16.57,2.3l-.7-23.53c5.2-1.44,9.9-2.72,14.97-1.72,6.24,1.24,10.35,6.22,9.46,12.87Z"/>
<path class="cls-6" d="M124.8,255.68c-2.58,5.08-7.97,7.88-13.45,8.97-.67.25-.86.62-1.12.47-.51-.3-.87-.74-.87-1.37v-16.49c4.96-1.3,9.32-.85,13.25,1.34,2.61,1.45,3.76,3.98,2.19,7.08Z"/>
<path class="cls-2" d="M129.95,257.42l-.77,1.9c-.22.54-.96,1.56-1.99.75,2.79-3,3.08-6.58,1.99-10.11.77.08,1.42-.13,1.42.09,0,.36-1.24,1.36.52,1.51-.36,1.63-.51,4.27-1.16,5.86Z"/>
<path class="cls-6" d="M106.69,248.96l-.68,8.48c-.13-.17-.33.27-.56.3l-.17-8.81,1.41.03Z"/>
<path class="cls-2" d="M138.89,279.18c-.37,2.3.5,4.35-.47,6.3-.1-.15-.32.27-.51.3l-.2-6.6h1.19Z"/>
<path class="cls-2" d="M105.85,278.54c1.16,2.43.51,4.28.28,6.27-1.31-1.86-1.24-4.15-.28-6.27Z"/>
<path class="cls-5" d="M220.24,229.03l-.6-.14-1.21-.28-2.43-.46c-8.06-2.53-16.09-3.31-24.73-2.08-1.96.28-2.28-4.19-1.56-5.38.71-6.92-.38-13.29-1.92-19.6l-.09-1.09"/>
<path class="cls-5" d="M76.87,236.81c.03-2.33-2.37-3.38-4.36-4.69l-.32-6.53c-5.46-9.7-.92-17.59-1.51-28.43l16.23,2.03,23.56-.02,16.48-1.23c2.97-.22,5.24-1.39,7.61.8,2.07,1.91,4.3,4.1,6.23,6.93l3.69-7.11,37.01-2.68c2.54-1.25,5.05-.41,6.58,1.69.16.02.28.22.42.36l30.55,29.94.79.78.4.39"/>
<path class="cls-5" d="M220.24,229.03l-.13.88-1.61,30.29-6.75,7.18,6.67,3.15,1.66,35.78c.09,1.99,0,4.05-1.49,5l-.49.39-.5.31-33.59,33.88-1.17,1.19"/>
<path class="cls-5" d="M182.84,347.08l-.74-.15-9.48-.44-13.17-.04c-5.51-.02-10.81.2-15.91-2.37-4.17,3.3-8.82,3.85-13.75,4.04l-11.57.44c-4.22.16-8-1.47-12.1-2.44l-9.23-2.18-2.37-2.53"/>
<path class="cls-5" d="M182.84,347.08l.4-1.16,1.57-4.51c2.75-7.94,3.11-16.57,1.63-25.15-.21-1.2,1.02-2.43,2.16-2.43l11.01.06c5.33.03,10.37-.9,15.23-2.19l3.17-.11"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #006d30;
}
.cls-2 {
fill: #00873e;
}
.cls-3 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.75px;
}
.cls-4 {
fill: #3a160a;
}
.cls-5 {
fill: #3d180d;
}
.cls-6 {
fill: #00a04b;
}
</style>
</defs>
<path class="cls-6" d="M153.66,195.96l-.46-.48-2.25.71,3.07,18.95.16,18.83c.02,1.82-1.11,3.58.04,5.52.86-1.54,1.21-3.1,1.51-4.81l4.63-26.09c.51-2.89.63-6.32-1.68-7.91,3.44-.33,1.92-4.68,4.63-6.02,1.06-.53,2.58-.34,3.85-.57l.57-.11.09,1.97c-1.23.02-2.43-.35-3.03.56,1.72-.21,4.69,1.64,3.78,3.07-1.78,2.8-2.55,5.21-1.44,8.53.2.59-.73,1.95-.05,2.92.42.6,1.37.66,2.45,1.35l.89,1.5c.22.37,1.97.27,1.95-.45-.06-2.39-4.42-4.98-2.14-5.98.89-.39,1.59-.33,2.08.47l3.78,6.21,3.2-1.66,6.99.65c-.45,5.85-2.86,9.72.48,15.56l8.02-1.38c4.46-.77,3.87,5.08,6.83,6.44-.37,2.19.28,4.05,1.64,6.45-6.86,3.73-8.55.88-9.78,1.54-2.84,1.52-2.36,6.78,0,8.07,2.28-5.08,5.7-2.99,8.83-5.88l3.1,3.95c.13.5-.52.62-.77.88-.49.54-1.76-.53-2.23-.28-1.18.63-.37,1.39-.08,1.77.39.5,2.4.95,4.35,1.17.61,1.74-1.58,2.73-2.4,3.52.4,2.18,3.63,1.16,5.22,1.12,1.87-.05,3.1,2.33,5.08,1.26l-.3,8.25c-7.48,2.34-14.03,3.49-21.14,4.16l-23.27,2.2c.09.33.06.66.03.83.7.61,1.85-.06,2.86-.16l30.5,2.09c2.57.18,5.12.05,7.54,1.41-6.47,3.24-11.29,0-12.25.87-.59.54-1.48,2.27-.13,2.72.91.3,1.87.35,2.3.69,1.2.95-1.2,5.83,1.12,6.74.78.3,2.4-.64,2.93.56.31.71.23,2.04-.94,2.69l-1.39,1.83c-1.39.83-1.28,1.3-1.07,2.69.24,1.57,1.51,1.36,3.53.89v3.28s4.12.29,4.12.29l.1,2.95c-4.9.84-9.59,2.46-12.77,6.65l-8.81-1.19c-7.44-1-2.19,11.29-2.46,19.21l-4.89-.16c-2.63-.09-6.35,0-7.58-2.4,1.86-2.75-1.3-5.62-.12-9.66-1.36-.25-2.17-1.48-3.52-1.43-2.58,1.83-5.92,2.68-6.55,5.79-.14.7,1.35,1.6.78,2.39-.88,1.22-5.4.29-5.27,3.28.14,3.32,3.47,1.65,4.91,3.68-1.55,1.93-3.69,4.09-5.51,5.41-.67.04-2.1-.08-2.75-.12l-7.79-.48-1.91-10.68-.87-20.54c-.02-.38.98-.89.38-1.07-.21-.06-.37-.3-.58-.52-.15-.16-.64.62-.67.95l-1.71,16.74c-.72,7.06-2.02,13.71-4.34,20.67-4.06.6-8.21-.87-12.16-2.67-.9-.41-2.41-.75-2.22-2.2.46-.15,1.4-.34,1.49-.82.06-.36.32-1.32-.43-1.33l-9.76-.09c-.43,0-.7-1.15-.36-1.47.61-.58,1.76-.61,2.03-1.46.31-.96.25-1.75.13-3.16l-14.56.05c-3.39.01-6.59.16-10.04-.64l12.59-14.11c-.78-.51-1.89.32-2.63,1.01-4.11,3.83-8.08,7.52-13.71,8.92.38-1.11,1.48-1.61,2.04-2.49l-1.39-4.29c-2.64.06-2.8-2.85-3.9-3.74-2.66-2.14-5.75-3.87-5.86-7.47-.13-4.69,5.58-4.68,4.87-7.46-1.89-.09-2.79-.22-3.53-1.67-2.76-5.4-5.55-10.74-6.13-16.95l.26-3.84c1.78-1.26,3.89-1.22,6.04-1.61l19.34-3.5c-7.89-1.44-15.42-1.63-22.88-4.1l-3.59-.27c-1.87-2.75-1.64-5.92-2.07-9l-1.2-8.62-.43-6.7c-.53-8.33-.99-6.87,3.53-10.65l3.35-2.87.41.38-.41-.38-6.25-5.77,1.99-24.45,29.01,2.92c.3,2.58-.26,4.92.22,7,.52-.09,1.76-.17,2.02.58.15.44.4.77.33,1.31l3.48,3.71c1.73,1.85,4.57,3.61,5.86,5.97,1.68,3.06,3.62,8.54,4.74,8.09.28-.11.94-.29,1.08-1.02.16-.85-.78-2.14-.85-3.37l-.49-9.68c-1.06-2.47-3.15-4.98-2.29-7.94l7.42-2.99.74-.79c.25-.26.12-.53-.1-.59-.25-.07-.86-.46-1.49.09l-.35-1.99,28.3-2.23c1.58-.12,3.1-.91,3.9,1l.46.48ZM186.99,284.59c.05-.8-.21-2.93-.89-3.46-1.03-.8-2.09-.08-2.67.96-2.92,5.19-8.02,9.43-14.12,9.6-10.92.3-16.23-12.43-14.77-24.11,1.5-12,9.42-26.8,19.63-23.04,6.4,2.36,4.16,10.81,8.14,8.64,1.03-.56,1.08-3.75.59-6.65,1.56,1.07,1.13,2.55,1.92,3.73.35.51,1.07.12,1.41-.26,1.54-1.69-1.42-11.67-11.3-14.47-18.44-5.21-31.37,21.26-30.31,39.87.46,8.02,4.55,18.71,12.73,22.74,12.45,6.13,27.46.08,33.05-12.33.29-.63.45-1.54.17-1.99s-.96-1.1-1.65-.81c-.59.25-1.21.83-1.92,1.58ZM101.07,245.27c-.97-.28-1.36,2.51-.27,3.04.84.41,1.82.3,3.43.02l-.02,15.84.52,28.42c.02,1.26-3.05.8-3.99,2.17-.37.54-.43,1.53.14,1.9.48.31,1.1.34,2.1.38l1.74.07c.21,0-.06,1.21-.78.9-1.53-.66-2.37,1.04-2.06,2.1.54,1.85,3.63,1.46,6.98.54.54.72.85,2.18,1.64,2.22.74.04,1.45-.09,1.73-.57.43-.71.32-1.49.26-2.52l13.46-1.99,15.07-1.37c.5-.05,1.21-.93,1.18-1.32-.02-.33-.33-.94-.83-.93l-6.64.11v-.92s7.21-.58,7.21-.58c.67-.05,1.07-1,1.09-1.41s-.74-1.12-1.09-1.32l-2.18-1.26c-7.01-.15-13.57.87-20.29,1.77l-6.33.85-.28-18.72c1.4-.01,2.1-.11,2.8-.21l14.99-2.18c1.84-.27,2.77-2.22,2.67-4.08-1.51-.62-2.4-.02-3.68-.25.35-1.33,1.22-1.8.93-2.28-.46-.75-.97-1.35-1.81-1.22l-16.69,2.55v-17.6c8.76-1.96,17.15-2.99,25.82-3.25.13,0,3.38-2.78,2.38-4.14-.56-.77-2.05-.63-3.18-.4-.1-1.28.99-1.47.68-1.91-.6-.86-1.04-1.38-1.97-1.52-8.39.05-16.34,1.7-24.78,2.69-.47-.45-.8-1.64-1.29-1.61-.68.04-.97.86-1.59,1.72-.59-.64-1.13-1.26-1.75-1.19-.85.11-1.69,0-1.57,1.15.13,1.23-2,1.2-2.96,1.52l-2.84.93c-1.36.44-1.02,1.83-.51,2.86l5.73-.22-1.66.49c-.18.57-.19,1.1-1.51.72Z"/>
<path class="cls-2" d="M189.09,192.96c.05.24.06.17.06-.01l.12.59,1.48,13.12.05,15.42c9.75-1.12,18.49.18,27.17,2.92l.66-.85,1.15,1.22.02.65-.12,6.48c-.06,3.28-.1,6.15-.34,9.4l-1.49,20.28c-.14,1.92.53,4.27-.65,6.12l-5.41,3.97,6.02,6.36,1.49,21.56c.37,5.41,2.59,8.94-3.33,12.4.34.27.29,1.3.03,1.53s-.16.34-.97.66c-.37,1.66-2.22,1.95-3.2,2.95l-20.55,20.92c-1.09,1.11-1.62,2.3-2.99,2.91-1.44.64-2.89,1.38-4.34,2.88l-11.83-.23-21.18-.57c-2.81-.08-5.69-.28-7.16-3.49l-2.05-.16c-3.58,7.25-10.93,5.75-18.03,6.21-3.56.23-7.24,1.2-10.75.28l-13.96-3.64c-3.32-.87-4.74-4.5-6.28-4.44-1.22.05-1.49,1.13-2.18,1.92l-3.06,3.53c-1.81,2.08-4.75,1.11-7.01.74l-7.91-1.31c-.88-.14-1.36-1.64-2.36-2.17.88-6.23-.67-11.82-2.56-17.57-.32-.99.84-5.27,1.01-8.28l.29-4.88.53-6.71.52-27.67,1.82-3.79c.68.02,1.42.1,2.03.75l-.26,3.84c.58,6.2,3.37,11.55,6.13,16.95.74,1.45,1.64,1.58,3.53,1.67.71,2.78-5.01,2.77-4.87,7.46.1,3.59,3.19,5.33,5.86,7.47,1.1.88,1.26,3.8,3.9,3.74l1.39,4.29c-.55.88-1.66,1.38-2.04,2.49,5.64-1.4,9.6-5.08,13.71-8.92.74-.69,1.84-1.52,2.63-1.01l-12.59,14.11c3.44.8,6.65.65,10.04.64l14.56-.05c.11,1.41.17,2.2-.13,3.16-.27.85-1.42.88-2.03,1.46-.33.32-.07,1.46.36,1.47l9.76.09c.76,0,.5.97.43,1.33-.08.47-1.03.66-1.49.82-.2,1.44,1.31,1.79,2.22,2.2,3.95,1.79,8.1,3.27,12.16,2.67,2.32-6.96,3.62-13.61,4.34-20.67l1.71-16.74c.03-.33.52-1.11.67-.95.21.23.36.46.58.52.6.17-.4.69-.38,1.07l.87,20.54,1.91,10.68,7.79.48c.64.04,2.08.16,2.75.12,1.82-1.32,3.96-3.47,5.51-5.41-1.45-2.03-4.77-.36-4.91-3.68-.13-2.98,4.38-2.06,5.27-3.28.57-.79-.92-1.69-.78-2.39.63-3.11,3.98-3.96,6.55-5.79,1.36-.05,2.16,1.19,3.52,1.43-1.18,4.04,1.98,6.9.12,9.66,1.23,2.39,4.95,2.31,7.58,2.4l4.89.16c.28-7.91-4.98-20.21,2.46-19.21l8.81,1.19c3.18-4.19,7.87-5.81,12.77-6.65l-.1-2.95-4.12-.29v-3.28c-2.02.46-3.28.68-3.52-.89-.21-1.38-.32-1.86,1.07-2.69l1.39-1.83c1.17-.65,1.25-1.97.94-2.69-.53-1.2-2.15-.26-2.93-.56-2.32-.9.09-5.78-1.12-6.74-.43-.34-1.39-.39-2.3-.69-1.35-.45-.46-2.18.13-2.72.95-.87,5.78,2.37,12.25-.87-2.42-1.36-4.97-1.23-7.54-1.41l-30.5-2.09c-1,.11-2.15.77-2.86.16.03-.17.07-.51-.03-.83l23.27-2.2c7.11-.67,13.66-1.83,21.14-4.16l.3-8.25c-1.98,1.07-3.21-1.31-5.08-1.26-1.6.04-4.83,1.06-5.22-1.12.82-.79,3.01-1.78,2.4-3.52-1.94-.22-3.96-.67-4.35-1.17-.3-.39-1.1-1.14.08-1.77.47-.25,1.75.82,2.23.28.24-.27.9-.39.77-.88l-3.1-3.95c-3.13,2.9-6.55.8-8.83,5.88-2.36-1.3-2.84-6.55,0-8.07,1.24-.66,2.93,2.18,9.78-1.54-1.36-2.4-2.01-4.25-1.64-6.45-2.96-1.36-2.37-7.21-6.83-6.44l-8.02,1.38c-3.34-5.83-.94-9.71-.48-15.56l-6.99-.65-3.2,1.66-3.78-6.21c-.48-.79-1.18-.86-2.08-.47-2.29,1,2.08,3.59,2.14,5.98.02.72-1.73.82-1.95.45l-.89-1.5c-1.07-.69-2.02-.75-2.45-1.35-.68-.97.25-2.32.05-2.92-1.11-3.33-.34-5.73,1.44-8.53.91-1.43-2.05-3.28-3.78-3.07.6-.91,1.8-.54,3.03-.56l-.09-1.97-.57.11.57-.11,13.46-1.04c2.81-.22,5.44-.4,7.96,0,0,.25,0,.18-.06.01Z"/>
<path class="cls-6" d="M189.09,192.96c.13-.05.29.17.44.4l29.1,30.8-.66.85c-8.68-2.74-17.43-4.04-27.17-2.92l-.05-15.42-1.48-13.12-.12-.59c0,.18-.01.25-.06.01Z"/>
<path class="cls-2" d="M121,196.71l.35,1.99c.63-.55,1.25-.16,1.49-.09.22.06.34.33.1.59l-.74.79-7.42,2.99c-.86,2.97,1.23,5.48,2.29,7.94l.49,9.68c.06,1.23,1,2.52.85,3.37-.14.73-.8.91-1.08,1.02-1.11.45-3.05-5.03-4.74-8.09-1.29-2.35-4.13-4.12-5.86-5.97l-3.48-3.71c.07-.54-.17-.88-.33-1.31-.26-.75-1.5-.67-2.02-.58-.48-2.08.07-4.42-.22-7,3.74.38,6.19,6.49,8.76,5.54,2.12-.78.47-6.55,7.47-6.94l4.09-.23Z"/>
<path class="cls-1" d="M153.66,195.96c1.59,1.69,3.16,3.44,5.02,4.72,2.32,1.59,2.2,5.02,1.68,7.91l-4.63,26.09c-.3,1.71-.65,3.27-1.51,4.81-1.16-1.94-.03-3.7-.04-5.52l-.16-18.83-3.07-18.95,2.25-.71.46.48Z"/>
<path class="cls-1" d="M73.83,272.96c-.61-.65-1.35-.72-2.03-.75l3.58-4.84-2.64-3.89,3.59.27c7.46,2.46,14.99,2.66,22.88,4.1l-19.34,3.5c-2.15.39-4.26.34-6.04,1.61Z"/>
<path class="cls-4" d="M101.07,245.27c1.32.37,1.33-.16,1.51-.72l1.66-.49-5.73.22c-.51-1.03-.85-2.41.51-2.86l2.84-.93c.97-.32,3.09-.29,2.96-1.52-.12-1.15.72-1.05,1.57-1.15.61-.08,1.16.55,1.75,1.19.62-.86.91-1.67,1.59-1.72.49-.03.82,1.16,1.29,1.61,8.43-1,16.39-2.64,24.78-2.69.93.14,1.37.67,1.97,1.52.31.44-.79.63-.68,1.91,1.13-.23,2.61-.37,3.18.4,1,1.35-2.25,4.13-2.38,4.14-8.67.26-17.05,1.29-25.81,3.25v17.6s16.68-2.55,16.68-2.55c.84-.13,1.36.47,1.81,1.22.29.47-.58.95-.93,2.28,1.28.22,2.17-.38,3.68.25.1,1.86-.83,3.81-2.67,4.08l-14.99,2.18c-.7.1-1.4.2-2.8.21l.28,18.72,6.33-.85c6.72-.9,13.28-1.91,20.29-1.77l2.18,1.26c.35.2,1.11.91,1.09,1.32s-.42,1.36-1.09,1.41l-7.21.58v.92s6.65-.11,6.65-.11c.5,0,.81.59.83.93.02.4-.69,1.28-1.18,1.32l-15.07,1.37-13.46,1.99c.07,1.03.17,1.8-.26,2.52-.29.48-.99.61-1.73.57-.79-.04-1.1-1.5-1.64-2.22-3.36.92-6.44,1.31-6.98-.54-.31-1.06.53-2.77,2.06-2.1.72.31,1-.89.78-.9l-1.74-.07c-.99-.04-1.62-.08-2.1-.38-.57-.37-.51-1.36-.14-1.9.94-1.37,4.01-.91,3.99-2.17l-.52-28.42.02-15.84c-1.61.28-2.59.39-3.43-.02-1.08-.53-.7-3.32.27-3.04Z"/>
<path class="cls-5" d="M186.99,284.59c.71-.75,1.33-1.33,1.92-1.58.69-.29,1.36.35,1.65.81s.12,1.36-.17,1.99c-5.59,12.41-20.6,18.47-33.05,12.33-8.18-4.03-12.28-14.72-12.73-22.74-1.06-18.61,11.87-45.08,30.31-39.87,9.88,2.79,12.84,12.78,11.3,14.47-.34.38-1.07.77-1.41.26-.8-1.18-.36-2.66-1.92-3.73.49,2.89.44,6.09-.59,6.65-3.98,2.17-1.74-6.28-8.14-8.64-10.22-3.76-18.14,11.04-19.63,23.04-1.45,11.68,3.85,24.41,14.77,24.11,6.1-.17,11.2-4.41,14.12-9.6.59-1.04,1.65-1.76,2.67-.96.68.53.94,2.66.89,3.46-.24,0-.82-.26-1.15.32l-2.56,4.45.51.54c1.45-1.23,3.14-4.22,3.13-4.06l.07-1.24Z"/>
<path class="cls-1" d="M186.99,284.59l-.07,1.24c0-.16-1.68,2.83-3.13,4.06l-.51-.54,2.56-4.45c.33-.57.91-.32,1.15-.32Z"/>
<path class="cls-3" d="M219.78,225.37l-1.81-.37c-8.68-2.74-17.43-4.04-27.17-2.92l-.05-15.42-1.48-13.12-.19-.58"/>
<path class="cls-3" d="M219.78,225.37l.02.65-.12,6.48c-.06,3.28-.1,6.15-.34,9.4l-1.49,20.28c-.14,1.92.53,4.27-.65,6.12l-5.41,3.97,6.02,6.36,1.49,21.56c.37,5.41,2.59,8.94-3.33,12.4-7.99.64-14.33,1.69-23.27.55-5.4-.69-.74,10.29-4.65,22.7-.68,2.15-.83,4.01.26,5.73"/>
<path class="cls-3" d="M189.15,192.95c-2.52-.41-5.15-.22-7.96,0l-13.46,1.04-.57.11c-1.27.24-2.79.05-3.85.57-2.71,1.35-1.19,5.7-4.63,6.02-1.86-1.28-3.42-3.03-5.02-4.72l-.46-.48c-.8-1.91-2.32-1.12-3.9-1l-28.3,2.23-4.09.23c-7.01.39-5.35,6.16-7.47,6.94-2.57.94-5.02-5.17-8.76-5.54l-29.01-2.92-1.99,24.45,6.25,5.77.41.38.41.37"/>
<polyline class="cls-3" points="189.15 192.95 189.53 193.36 218.63 224.15 219.78 225.37"/>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #39170a;
}
.cls-2 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.75px;
}
.cls-3 {
fill: #3d180b;
}
.cls-4 {
fill: #a88a21;
}
.cls-5 {
fill: #d3ac2c;
}
.cls-6 {
fill: #ffcf34;
}
</style>
</defs>
<path class="cls-6" d="M179.77,205.02l.77,4.04c.04.21.1.45.08.59.02-.14-.04-.38-.08-.59-.85,1.19-1.1-.58-2.67-.25l.79,8.42-.89,10.4c-.41,4.82-1.24,9.97,1.41,14.45,1.05,1.78,9.63-.31,17.16-.75,1.02,1.74,4.84.68,6.63,4.83.88,2.04,2.91,3.45,3.86,5.38,1.4,2.81,2.25,5.73,4.84,7.45,1.02-.82,1.88-.07,2.66-.28l.07,6.27c.03,2.58.2,4.71-.47,6.93-.75.09-2.1-.23-2.94.31-2.87,1.83-6.08,2.33-9.57,2.78l-6.5.84-16.99.61c-3.66.13-7.28-.69-11.12,1.38l16.47,2.61,22.92,3.07,7.37.46c.9,1.94.62,4.11.83,6.29l1.64,17c.53,5.45-8.08,6.66-5.27,8.93,2.01,1.62,5.55,12.81,5.16,16.41l-2.13,19.73c-8.18,1.27-16.4,2.09-24.72,1.88l-6.37-8.74c-.46-.63-1.12-1.06-1.64-1.08-.67-.03-1.56.51-2.14,1.26l-5.23,6.74-16.53,1.49c-2.64.24-5.37.61-7.42-.64l-.56-.34c.37,0,.52-.6.99-.96.23-.18-.52-.27-.45-.87l-1.23-17.67c-.63-9.07-.23-17.95-2.92-26.95l-2.35,17.78-4.21,27.18,3.26.18c-1.16,2.56-3.95,2.42-6.56,2.51l-13.67.49-16.87,1.08c-.05-.45-.49-.46-.65-.67l-.4-.38-.4-.38-.4-.38.58-.54.2.56c2.01-.21.19-3.36.22-9.43,0-2.13-.72-4.28.05-6.44.35-.98,2.07-.69,2.87-.56.43-1.64-1.95-1.67-2.13-2.33l.95-10.42c.09-.94.1-1.71-.31-2.02-1.64-1.25-3.51.15-5.41-.22.06-.7-.29-1.6-.74-2.1-.64-.71-1.77-.71-2.91-.62l-12.92,1.07.18-1.34s-.25-1-.63-1.37l-3.78-3.63c-1.39-1.34-2.03-2.83-1.58-5.16l-3.26-3.67c-1.51-1.7-2.88-4.39-.81-6.29,1.1-.32,3.6.81,4.46-.47,2.86-4.24-1.31-5.58-.49-7.74,1.33-3.48-4.58-6-4.26-8.5l13.01-2.5c3.67-1.59,6.78-3.8,10.59-3.22l.1,32.81c0,1.01,1.11,1.93,1.68,2.01,2.03.27,1.28-3.03,2.08-6.24,2.13.46-1.02,4.51,1.57,8.01.45.61,1.46,0,1.84-.26.62-.41.57-1.41.77-2.39l1.09-.2c.09-.02.41-.44.4-.93l-.43-14.35-.45-23.53c-.01-.52-.02-1.19.34-1.48.58-.46,1.53-.32.75.63l8.86,15.58,13.62,24.51c.37.67,1.86,1.46,2.5,1.41.85-.07,1.58-1.73,1.05-2.73-.13-1.47.35-1.05,1.48-.63,1.09-.52,1.88.67,2.48.34.91-.51,1.18-1.31,1.03-2.48-1.7-13.24-2.12-26.26-1.41-39.55l.91-17.03c.05-.93-.07-1.64-.43-2.01-.49-.49-1.44-.78-1.91-.55-.75.36-1.15,1.14-1.13,2.02l.19,7.68h-.86s-.48-6.72-.48-6.72c-.08-1.09-1.4-1.77-2.14-1.88-1.31-.2-2.03,1.16-2.03,2.59l-.14,40.7c-1.18.02-.63-.49-.97-1.13-7.29-13.26-14.57-26.19-20.99-39.77-.21-.45-.86-.62-1.11-.66-.4-.05-.83.64-1.21.96l-.5-2.31c-.16-.75-1.63-1.47-2.45-.7-1.35,1.27-.5,3.3-.84,4.65-1.25-.1-.52-1.14-.93-1.47-.51-.4-1.14-.55-2.26-.76l-.37,3.69c-2.89-2.03-5.73-4.3-9.04-6.32-.72-.34-2.29.61-2.32,1.39-.03,1.15-.89,2.45.54,3.02l10.66,3.95.41,19.15-14.09-.14c-5.14-.17-9.74-2.39-14.67-4.07-.85.62-1.32,1.36-2.39.69l-1.67-18.92c-.18-2.03.78-3.84,2.65-4.71l7.42-3.44-5.93-5.13.16-5.46c-1.39-3.37-3.58-6.69-3.35-10.39l1.05-16.95c.05-.8-.56-1.46-.02-2.1s1.18.05,2.02.15l15.08,1.79,14.48.61,27.87-1.53c2.78-.15,4.85.21,6.45,2.28-.81,1.03-1.69-.01-3.26,0l4.13,7.93c4.58,11.64,3.97,31.83,8.32,46.37l-.45-23.47,1.02-30.81c-.29-.23-.77.13-1.3.05-.68-.1.4-.6.29-1.64l-.63.29.63-.29,16.21-1.53,9.43-.31c4.27-.14,8.52-1.66,12.53-.39l.66.23c-.03.16-.03.22-.03-.04ZM183.3,260.1c.11.72.67,3.38,2.19,2.35,1.76-7.21-5.95-14.22-14.14-15.14-14.41-1.63-23.52,15.84-26.38,29.41-2.46,11.72.92,27.85,11.7,33.33,3.81,1.94,8.55,3.2,12.6,2.71,17.38-2.11,23.73-18.78,19.2-17.57-1.69.45-1.96,3.32-2.63,3.58-2.37.93.71-2.54.04-4.79-.24-.81-1.17-1.31-1.71-.96-2.42,1.56-3.83,8.02-11.84,10.21-7.31,2.01-14.11-2.14-16.6-9.5-6.23-18.48,5.85-40.89,17.78-36.88,5.29,1.78,5.29,9.24,6.76,9.31,2.39.13,2.79-4.52,2.18-6.12l.85.05Z"/>
<path class="cls-5" d="M97.21,275.95c.18.35.62.79.03.98-3.81-.59-6.93,1.63-10.59,3.22l-13.01,2.5c-.32,2.5,5.59,5.02,4.26,8.5-.82,2.16,3.35,3.5.49,7.74-.86,1.28-3.37.15-4.46.47-2.07,1.9-.69,4.59.81,6.29l3.26,3.67c-.45,2.32.19,3.82,1.58,5.16l3.78,3.63c.38.37.64,1.34.63,1.37l-.18,1.34c-5.9.49-11.78-1.34-17.6-2.56,0,.16,0,.27,0,.02l-.69-.73c.06-1.46-.73-4.12-.11-5.67.88-2.2.34-3.61.48-5.56l1.71-23.37c.25-3.49-1.22-7.09-1.53-10.53,1.07.67,1.54-.07,2.39-.69,4.93,1.69,9.53,3.9,14.67,4.07l14.09.14Z"/>
<path class="cls-6" d="M83.8,320.81l12.92-1.07c1.13-.09,2.26-.09,2.91.62.45.5.8,1.4.74,2.1-.91,10.45-.01,20.93,3.56,30.87l-.58.54-36.74-35.2-.4-.41c5.82,1.22,11.7,3.05,17.6,2.56Z"/>
<path class="cls-6" d="M179.77,205.02c0,.18,0,.24.03.04l34.21,33.44-.49.54-1.92-.35c-.22-.04-.45-.09-.64-.11l-2.66-.27c-13.98-3.98-24.18.77-25.99-.65-.73-.57-.59-1.87-.82-2.93,1.42-8.61.58-16.85-.87-25.09.02-.14-.04-.38-.08-.59l-.77-4.04Z"/>
<path class="cls-5" d="M149.77,353.23l-.56-.34-3.01-2.1c-1.49-1.04-3.18-.66-3.91.8l-3.26-.18,4.21-27.18,2.35-17.78c2.69,9,2.29,17.87,2.92,26.95l1.23,17.67c-.07.59.69.68.45.87-.47.37-.62.97-.99.96l.56.34Z"/>
<path class="cls-5" d="M140.36,207.35l.63-.29c.11,1.04-.97,1.54-.29,1.64.53.08,1.01-.28,1.3-.05l-1.02,30.81.45,23.47c-4.34-14.54-3.74-34.72-8.32-46.37l-4.13-7.93c1.57-.02,2.45,1.02,3.26,0,1.15,1.48,3.05,3.31,5.03,3.99l3.08-5.27Z"/>
<path class="cls-5" d="M213.94,271.89c-1.02,3.42-5.64,5.2-4.81,6.04l3.77,3.83c.8.81.39,1.56.68,2.19l-7.37-.46-22.92-3.07-16.47-2.61c3.84-2.08,7.46-1.25,11.12-1.38l16.99-.61,6.5-.84c3.49-.45,6.7-.95,9.57-2.78.84-.53,2.19-.22,2.94-.31Z"/>
<path class="cls-5" d="M214.34,258.7c-.77.21-1.64-.54-2.66.28-2.59-1.73-3.44-4.65-4.84-7.45-.96-1.92-2.99-3.34-3.86-5.38-1.79-4.15-5.61-3.09-6.63-4.83l14.51-.85c-.03-.62-.15-1.25.1-1.89.19.02.43.07.64.11l1.92.35.49-.54.4.39c.14.14.38.23.4.39l.1.72c.48,3.66-.57,7.44-.57,11.31v7.39Z"/>
<path class="cls-1" d="M96.95,254.77l.37-3.69c1.12.21,1.75.37,2.26.76.42.33-.32,1.37.93,1.47.34-1.34-.52-3.38.84-4.65.82-.77,2.29-.05,2.45.7l.5,2.31c.37-.32.81-1.01,1.21-.96.25.03.9.2,1.11.66,6.43,13.58,13.7,26.51,20.99,39.77.33.64-.22,1.15.97,1.13l.14-40.7c0-1.44.72-2.79,2.03-2.59.75.11,2.07.79,2.14,1.88l.48,6.73h.86s-.19-7.69-.19-7.69c-.02-.88.38-1.66,1.13-2.02.47-.23,1.41.06,1.91.55.36.36.48,1.08.43,2.01l-.91,17.03c-.71,13.29-.29,26.3,1.41,39.55.15,1.17-.12,1.97-1.03,2.48-.6.34-1.39-.86-2.48-.34-1.13-.42-1.61-.84-1.48.63.53,1-.2,2.66-1.05,2.73-.63.05-2.13-.74-2.5-1.41l-13.62-24.51-8.86-15.58c.78-.95-.17-1.09-.75-.63-.36.29-.35.96-.34,1.48l.45,23.53.43,14.35c.01.49-.31.91-.4.93l-1.09.2c-.2.98-.16,1.98-.77,2.39-.38.26-1.39.87-1.84.26-2.59-3.5.56-7.55-1.57-8.01-.8,3.22-.05,6.51-2.08,6.24-.58-.08-1.68-1-1.68-2.01l-.1-32.81c.59-.19.15-.63-.03-.98l-.41-19.15c-.01-.67.09-1.35.16-2.03ZM131.61,294.87c.56.17.86.1,1.15.05l-.14-5.48h-.88s-.12,5.43-.12,5.43Z"/>
<path class="cls-3" d="M183.3,260.1l-.32-2.09c-.11-.72-1.11-1.15-.97-1.75l-3.04-3.05c-.51-.51-.92-.45-1.63-.42,2.53,1.87,4.03,4.39,5.11,7.26.61,1.6.21,6.25-2.18,6.12-1.47-.08-1.46-7.54-6.76-9.31-11.93-4-24.01,18.41-17.78,36.88,2.48,7.36,9.28,11.51,16.6,9.5,8-2.2,9.41-8.66,11.84-10.21.54-.35,1.47.15,1.71.96.66,2.24-2.41,5.72-.04,4.79.67-.26.94-3.13,2.63-3.58,4.53-1.21-1.83,15.46-19.2,17.57-4.05.49-8.79-.77-12.6-2.71-10.78-5.48-14.16-21.61-11.7-33.33,2.86-13.58,11.96-31.05,26.38-29.41,8.2.93,15.9,7.93,14.14,15.14-1.52,1.03-2.08-1.63-2.19-2.35Z"/>
<path class="cls-4" d="M180.62,209.65c1.45,8.24,2.29,16.48.87,25.09.23,1.06.09,2.35.82,2.93,1.81,1.42,12.02-3.33,25.99.65l2.66.27c.19.02.43.07.64.11-.22-.04-.45-.09-.64-.11-.26.64-.13,1.27-.1,1.89l-14.51.85c-7.53.44-16.11,2.54-17.16.75-2.65-4.48-1.82-9.62-1.41-14.45l.89-10.4-.79-8.42c1.56-.33,1.82,1.45,2.67.25.04.21.1.45.08.59Z"/>
<path class="cls-4" d="M100.37,322.45c1.9.37,3.77-1.03,5.41.22.41.31.4,1.07.31,2.02l-.95,10.42c.18.66,2.56.69,2.13,2.33-.8-.12-2.52-.42-2.87.56-.77,2.16-.04,4.31-.05,6.44-.03,6.07,1.79,9.23-.22,9.43l-.2-.56c-3.58-9.94-4.47-20.41-3.56-30.87Z"/>
<path class="cls-5" d="M96.95,254.77c-.07.68-.17,1.36-.16,2.03l-10.66-3.95c-1.43-.57-.57-1.87-.54-3.02.02-.78,1.6-1.73,2.32-1.39,3.31,2.02,6.15,4.3,9.04,6.32Z"/>
<path class="cls-4" d="M183.3,260.1l-.85-.05c-1.08-2.87-2.59-5.4-5.11-7.26.71-.03,1.12-.09,1.63.42l3.04,3.05c-.15.6.86,1.03.97,1.75l.32,2.09Z"/>
<path class="cls-4" d="M131.61,294.87l.12-5.43h.88s.14,5.47.14,5.47c-.29.05-.59.12-1.15-.05Z"/>
<path class="cls-2" d="M179.77,205.02l-.62-.2c-4-1.27-8.26.25-12.53.39l-9.43.31-16.21,1.53-.63.29-3.08,5.27c-1.99-.68-3.89-2.51-5.03-3.99-1.6-2.07-3.66-2.43-6.45-2.28l-27.87,1.53-14.48-.61-15.08-1.79c-.84-.1-1.46-.81-2.02-.15s.07,1.3.02,2.1l-1.05,16.95c-.23,3.7,1.96,7.02,3.35,10.39l-.16,5.46,5.93,5.13-7.42,3.44c-1.86.87-2.83,2.67-2.65,4.71l1.67,18.92c.3,3.43,1.78,7.04,1.53,10.53l-1.71,23.37c-.14,1.96.39,3.36-.48,5.56-.62,1.55.17,4.21.11,5.67l.69.73"/>
<path class="cls-2" d="M214.8,239.27l-1.28-.24-1.92-.35c-.22-.04-.45-.09-.64-.11l-2.66-.27c-13.98-3.98-24.18.77-25.99-.65-.73-.57-.59-1.87-.82-2.93,1.42-8.61.58-16.85-.87-25.09.02-.14-.04-.38-.08-.59l-.77-4.04"/>
<path class="cls-2" d="M104.54,355.01l-.41-1.13-.2-.56c-3.58-9.94-4.47-20.41-3.56-30.87.06-.7-.29-1.6-.74-2.1-.64-.71-1.77-.71-2.91-.62l-12.92,1.07c-5.9.49-11.78-1.34-17.6-2.56"/>
<polyline class="cls-2" points="179.8 205.06 214.01 238.49 214.41 238.88 214.8 239.27"/>
<path class="cls-2" d="M214.8,239.27l.1.72c.48,3.66-.57,7.44-.57,11.31v7.39s.07,6.27.07,6.27c.03,2.58.2,4.71-.47,6.93-1.02,3.42-5.64,5.2-4.81,6.04"/>
<polyline class="cls-2" points="66.21 318.28 66.6 318.66 103.34 353.86 103.74 354.24 104.14 354.63 104.54 355.01"/>
<path class="cls-2" d="M210.79,316.19c2.01,1.62,5.55,12.81,5.16,16.41l-2.13,19.73c-8.18,1.27-16.4,2.09-24.72,1.88l-6.37-8.74c-.46-.63-1.12-1.06-1.64-1.08-.67-.03-1.56.51-2.14,1.26l-5.23,6.74-16.53,1.49c-2.64.24-5.37.61-7.42-.64l-.56-.34-3.01-2.1c-1.49-1.04-3.18-.66-3.91.8-1.16,2.56-3.95,2.42-6.56,2.51l-13.67.49-16.87,1.08-.65-.67"/>
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #9b1f0f;
}
.cls-2 {
fill: #3a160a;
}
.cls-3 {
fill: #e93525;
}
.cls-4 {
fill: #3d180d;
}
.cls-5 {
fill: #c12b1c;
}
.cls-6 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2px;
}
</style>
</defs>
<path class="cls-3" d="M141.35,207.84c-.15-.1-.38-.08-.57-.14-.5.53.56,1.73-.35,2.63,2.2,6.52,2.73,13.17,3.12,20.16l1.18,20.95c.3-.07.67-.04.84-.02.7-.89-.2-2.18-.08-3.4l3.51-37.22c.07-.78-.41-1.44-.5-1.88l19.5-2.14c1.96.84,1.89,4.16,2.43,5.54-.19-.36-1.21.01-1.82.21-.39.13.11.72-.4,1.5.58,10.03-3.74,18.54-4.28,28.36,3.88-6.54,14.81-30.94,15.78-31.51,1.1-.65,1.54.47,2.43.78,1.28.45,3.34-.37,3.73,1.34.25,1.09,1.66.7,2.44,1.56-.46,1.49-1.33,2.83-1.48,4.39,3.42.29,7.69-.88,11.02.3,2.37.84-.91,3.18-1.22,4.37.17,4.04,4.98,4.47,10.21,3.99.82.43.92,2.65,2.23,2.64l-4.61,3.64c4.87-.22,8.3,1.84,11.15,1.44l-.61,4.42c-.69.13-1.97-.34-2.8.24-6.06,4.22-12.42,7.63-18.95,11.04l-6.29,4.39c4.87.92,9.07-1.38,13.76-2.48,3.75-.88,7.71-1.15,11.61-1.17,1.47,0,3.02,1.79,4.47.65l-1.19,12.44c-.25,2.67-.6,5.14-1.34,7.45-.83-.98-1.63-.31-2.63.07-6.58,2.5-13.24,3.89-20.13,5l-17.39,2.78c1.75,1.3,3.52.34,5.24.34h32.08c-1.46,8.73-7.23,6.14-7.99,8.08-1.21,3.1,2.92,5.92,1.74,7.9s-3.15,3.27-4.07,5.7c2.08.02,5.13-3.57,7.39-2.41.77.39,1.39,1.77,1.11,3.19-3.36,4.96-9.42,5.44-8.61,13.35l-.74,3.75c-1.58.06-2.1,1.03-2.21,2.08l-12.24-1.78c-1.47-.21-4.87-.09-4.63,1.94l1.6,13.76c.17,1.43.76,2.77-.25,3.79l-5.27,2.54-2.14.63-1.93-1.16c-6.66,3.78-4.71,7.07-15.02,6.31l-11.95,2.56-1.39-7.83c-3.15-7.67-3.81-15.27-3.95-23.41l-.15-8.71c-.38.04-.44-.34-.87-.53l-1.62,20.27c-.21,2.63-.8,4.53-1.38,7.39l-20.37-.61c-2.55-.08-4.58-3.05-7.89-1.97-.6-2.94-2.88-3.36-5.33-3.82.34-4.53,4.71-6.78,3.51-8.96l-7.49,7.73-1.13-2.09c-1.15-2.13-1.4-5.14-4.15-6.09-.73-1.72-2.1-3.05-4.58-2.9-.17-1.75-2.45-1.79-2.86-2.71-.24-.54.43-.87-.61-.57-.2.06-3.02-3.72-3.62-7.19-3.05-.69-.25-4.22-2.63-4.24,3.16-3.19-3.84-1.11-4.81-3.41-.81-1.91-.04-3.39.74-4.96.26-.51-.56-4.29,2.44-9.1,6.33-2.1,12.4-3.33,18.8-4.23.25-.04.88.64,1.17.5.34-.16.76-.41.89-1.14l-22.09-1.45-1.42-.42c.68.2-2.27-4.82-.93-7.13,1.88-.95,4.57-.3,6.65-.77.49-.11,4.68-5.32,4.6-4.85.15-.81-.35-1.49-.9-1.62-1.08-.25-1.49,2.01-5.22,2.29-1.4.11-3.82-3.52-3.86-3.9-.09-.81.64-.76,1.09-1.56,3.25-5.81,2.03-5.22,1.61-9.67-.13-1.35-1.69-2.4-.19-3.82,2.65-2.49,5.15-5.35,6.94-8.32.25-.1.64-.51,1.11-.47l13.86,1.31c3.55-2.76,1.46-9.93,1.27-15.68l-.49-14.85c.39-.69.33-1.35.37-1.85.06-.7-.98-1.5-1.55-.94l.55-2.93.11-.58.22-1.16-.39.4.39-.34c4.32.66,9.46,1.37,13.92,1.69l14.97,1.06c3.88.27,7.66-.03,11.21,1.08.19.06.42.03.57.14ZM102.09,290.2l.93,21.66c.06,1.44-.52,3.82,1.01,4.64.98.52,2.05,1,2.45.38l1.25-1.93c.34-.52-.14-1.15.09-1.4.18-.19.64-.12.85-.03.32.12.23.16.26.39.08.53-.64.83-.5,1.16.41.96,1.63,1.3,2.11.96.43-.3,1.27-1.06,1.24-1.95l-.87-22.21c8.59-.6,16.52-3.59,22.83-8.98,7.59-6.48,9.09-17.7,2.59-25.38s-21.36-8.59-30.78-5.14c-.19-1.38-.35-2.08-.8-2.15l-2.07-.29-.32,3.56c-9.69,4.25-8.22,8.19-6.55,7.13.37-.23.75-.79,2.02-.94l-1.7,2.61c-.33.5-.28,1.36.15,1.65.29.2,1.12.33,1.58,0l4.17-3.06-.03,24.78c-1.77.34-3.13-1.12-4.55-.4-1.95.98.56,3.2,4.63,4.93ZM183.9,295.43c-2.54-1.64-4.08,8.31-13.8,10.5-5.05,1.14-10.5-1.01-13.47-5.57-4.78-7.32-5.07-16.43-2.58-24.6,3.13-10.28,10.83-20.05,19.27-15.53,5.81,3.12,2.84,9.38,6.76,8.1.76-.25,1.62-3.66.55-6.94,1.78.38,1.35,3.89,2.76,4.16,2.82.54.95-11.97-10.88-15.11-17.71-4.7-30.43,20.87-29.61,38.76.36,7.72,2.8,15.24,8.44,20.4,5.98,5.47,14.37,6.55,22.33,4.17,9.83-2.94,16.54-13.86,14.45-15.95-.52-.53-1.42-.6-1.75-.07l-.88,1.4c-.87.17-.68.78-.87.89-1.52.83,1.03-3.48-.73-4.61Z"/>
<path class="cls-5" d="M100.35,205.56l-.55,2.93c-1.08,5.75-2.71,11.68-2.27,17.86.25,3.54.85,6.96.03,10.68-8.89.06-16.45-2.38-25.75.39-.64.15-.28,1.16-.01,1.44.24.26.66-.03,1.36.22l11.41,1.34c.18.5.36.67.67.55-1.8,2.97-4.29,5.82-6.94,8.32-1.5,1.41.06,2.46.19,3.82.42,4.45,1.64,3.85-1.61,9.67-.45.81-1.18.76-1.09,1.56.04.39,2.46,4.01,3.86,3.9,3.73-.29,4.14-2.55,5.22-2.29.56.13,1.05.81.9,1.62.09-.47-4.1,4.74-4.6,4.85-2.08.48-4.77-.18-6.65.77-1.35,2.31,1.61,7.33.93,7.13l1.42.42,22.09,1.45c-.13.74-.55.98-.89,1.14-.29.14-.91-.53-1.17-.5-6.4.9-12.47,2.12-18.8,4.23-3,4.81-2.18,8.59-2.44,9.1-.78,1.57-1.55,3.04-.74,4.96.97,2.29,7.97.21,4.81,3.41,2.38.02-.42,3.55,2.63,4.24.6,3.47,3.42,7.25,3.62,7.19,1.04-.3.38.02.61.57.4.92,2.69.96,2.86,2.71,2.48-.16,3.85,1.18,4.58,2.9,2.75.95,3,3.96,4.15,6.09l1.13,2.09,7.49-7.73c1.2,2.18-3.16,4.43-3.51,8.96,2.45.45,4.73.87,5.33,3.82,3.31-1.07,5.34,1.9,7.89,1.97l20.37.61c.58-2.85,1.17-4.75,1.38-7.39l1.62-20.27c.43.19.49.57.87.53l.15,8.71c.14,8.14.8,15.74,3.95,23.41l1.39,7.83,11.95-2.56c10.32.76,8.36-2.53,15.02-6.31l1.93,1.16,2.14-.63,5.27-2.54c.4-.02.63.4.83.64.16.18-.17.6-.2,1.04l-.67,10.12c.82-.14,1.5-.45,2.27,0,2.81-6.65,3.02-13.87,2.67-21.4.15-.92-.09-2.34.39-2.85.45-.48,1.84-1.22,2.79-1.11,7.02.82,13.69.47,20.29-1.35,1.03.54,2.17.4,3.18.19.08.61.02,1.31-.5,1.83l-14.08,13.99c-3.04,3.02-6.73,5.26-9.07,8.88l-3.1,3.24c-1.03-1.01-2.03-.45-2.82-.97l-2.06,4.85-35.37-1.32c-4.34-1.79-6.54-8.04-7.73-7.92s-2.8,6.38-5.48,6.54l-10.41.64c-5.38.33-10.15.72-15.49,2.19-3.14.87-7.62.86-10.83.08-2.95-1.18-3.85-5.14-6.2-6.63-2.58.57-3.1,5.36-5.17,6.12s-15.98-.69-16.94-1.64c-1.04-1.02-1.66-2.74-1.93-4.36l-1.46-8.74c-.26-1.57-1.4-2.96-.05-5l.25-3.57c1.08-2.07.21-4.64.52-7.05l1.27-9.71c.24-1.86.5-4.12-.13-5.95l-.38-7.85c-.09-1.91-.47-3.76-.13-5.55l1.4-7.4c.42-2.24,1.09-4.38,1.82-6.51-.7-1.43-2.17-2.49-2.25-3.91l-.41-7.84-.51-7.74-.6-8.71-.3-7.65c-.86-4.02-1.08-8.37,2.47-11.45.28-.11.66-.01.92.02l30.19-31.05.84.55Z"/>
<path class="cls-5" d="M215.61,235.34c-2.85.4-6.28-1.66-11.15-1.44l4.61-3.64c-1.3,0-1.41-2.21-2.23-2.64-5.23.47-10.04.05-10.21-3.99.31-1.19,3.58-3.53,1.22-4.37-3.33-1.18-7.6,0-11.02-.3.14-1.57,1.01-2.91,1.48-4.39-.78-.86-2.19-.47-2.44-1.56-.39-1.71-2.45-.89-3.73-1.34-.89-.32-1.33-1.43-2.43-.78-.97.57-11.91,24.97-15.78,31.51.54-9.82,4.86-18.33,4.28-28.36.51-.78,0-1.37.4-1.5.61-.2,1.63-.57,1.82-.21.52,1.34,1.39,2.32,2.25,3.36l.35.42-.35-.42c2.99-4.01,4.32-9.78,6.79-11.04,1.99-1.01,6.98.63,10.05.99,2.25.26,4.67-.67,7.13.45l5.57.52c4.86.45,13.52,1.2,14.2,1.84.95.89.45,1.58.67,2.86,1.43,8.35.85,16.35-1.48,24.06Z"/>
<path class="cls-5" d="M214.27,272.31c-.4,1.25-2.02,2.41-1.74,4.07l2.61,3.32,1.18,21.98,1.27,18.14c.18,2.6-1.32,4.68-3.5,5.32-1,.22-2.14.35-3.18-.19.51-.44.48-1.12.22-2.05-4.36.28-8.77-.13-13.07-.76.11-1.05.62-2.02,2.21-2.08l.74-3.75c-.81-7.92,5.25-8.4,8.61-13.35.28-1.42-.34-2.79-1.11-3.19-2.26-1.16-5.3,2.44-7.39,2.41.92-2.43,2.85-3.65,4.07-5.7s-2.95-4.8-1.74-7.9c.76-1.94,6.53.65,7.99-8.08h-32.08c-1.72,0-3.48.96-5.24-.34l17.39-2.78c6.89-1.1,13.55-2.49,20.13-5,1-.38,1.8-1.05,2.63-.07Z"/>
<path class="cls-1" d="M148.5,208.91c.09.44.57,1.1.5,1.88l-3.51,37.22c-.12,1.22.79,2.51.08,3.4-.17-.02-.55-.05-.84.02l-1.18-20.95c-.39-6.98-.92-13.64-3.12-20.16.91-.89-.15-2.09.35-2.63.19.06.42.03.57.14,2.12,1.41,4.66,1.34,7.15,1.07Z"/>
<path class="cls-5" d="M215,239.76c-.37,2.69-1.06,4.75-4.09,5.88,0,1.06-1.32,1.19-1.19,1.7.25,1,1.38,1.43,2.04,1.7,1.6.65,5.23,1.29,5.03,3.38-1.45,1.14-3-.66-4.47-.65-3.89.02-7.85.3-11.61,1.17-4.69,1.1-8.9,3.39-13.76,2.48l6.29-4.39c6.53-3.41,12.89-6.83,18.95-11.04.83-.58,2.12-.11,2.8-.24Z"/>
<path class="cls-2" d="M102.09,290.2c-4.07-1.73-6.58-3.95-4.63-4.93,1.42-.72,2.78.75,4.55.4l.03-24.78-4.17,3.06c-.46.33-1.29.2-1.58,0-.43-.29-.48-1.15-.15-1.65l1.7-2.61c-1.27.15-1.65.71-2.02.94-1.68,1.06-3.15-2.89,6.55-7.13l.32-3.56,2.07.29c.45.06.61.77.8,2.15,9.43-3.46,24.09-2.78,30.78,5.14s5,18.9-2.59,25.38c-6.31,5.39-14.24,8.38-22.83,8.98l.87,22.21c.03.89-.81,1.65-1.24,1.95-.48.34-1.7,0-2.11-.96-.15-.34.58-.64.5-1.16-.04-.23.06-.27-.26-.39-.21-.08-.67-.16-.85.03-.23.25.25.88-.09,1.4l-1.25,1.93c-.4.62-1.47.15-2.45-.38-1.53-.82-.95-3.2-1.01-4.64l-.93-21.66ZM131.84,268.69c-.17-9.31-11.39-12.74-20.83-10.93-.29,9.08-1.11,17.3-.24,26.28,9.58-2.04,21.23-6.33,21.06-15.35Z"/>
<path class="cls-4" d="M183.9,295.43c1.76,1.14-.79,5.45.73,4.61.19-.11,0-.72.87-.89l.88-1.4c.33-.52,1.23-.45,1.75.07,2.08,2.09-4.63,13.02-14.45,15.95-7.96,2.38-16.35,1.3-22.33-4.17-5.64-5.17-8.09-12.68-8.44-20.4-.82-17.89,11.9-43.46,29.61-38.76,11.84,3.14,13.7,15.66,10.88,15.11-1.41-.27-.98-3.78-2.76-4.16,1.07,3.28.2,6.69-.55,6.94-3.93,1.28-.95-4.98-6.76-8.1-8.45-4.53-16.14,5.24-19.27,15.53-2.49,8.17-2.2,17.28,2.58,24.6,2.98,4.56,8.43,6.7,13.47,5.57,9.71-2.19,11.26-12.13,13.8-10.5ZM177.02,256.36l3.08,4.12c1.58-.8-.62-4.44-3.08-4.12Z"/>
<path class="cls-1" d="M198.07,322.14c4.3.63,8.71,1.03,13.07.76.25.92.29,1.6-.22,2.05-6.61,1.82-13.27,2.17-20.29,1.35-.95-.11-2.34.63-2.79,1.11-.48.51-.23,1.94-.39,2.85.34,7.53.14,14.75-2.67,21.4-.77-.46-1.44-.14-2.27,0l.67-10.12c.03-.44.36-.86.2-1.04-.2-.24-.43-.66-.83-.64,1.02-1.02.42-2.37.25-3.79l-1.6-13.76c-.24-2.03,3.16-2.15,4.63-1.94l12.24,1.78Z"/>
<path class="cls-1" d="M85.24,240.96c-.31.13-.49-.05-.67-.55l-11.41-1.34c-.71-.24-1.12.04-1.36-.22-.26-.28-.63-1.29.01-1.44,9.3-2.77,16.87-.34,25.75-.39.82-3.72.22-7.14-.03-10.68-.44-6.17,1.19-12.1,2.27-17.86.58-.57,1.61.24,1.55.94-.04.5.02,1.16-.37,1.85l.49,14.85c.19,5.76,2.28,12.92-1.27,15.68l-13.86-1.31c-.47-.04-.87.37-1.11.47Z"/>
<path class="cls-3" d="M131.84,268.69c.17,9.02-11.48,13.31-21.06,15.35-.87-8.98-.05-17.2.24-26.28,9.44-1.81,20.65,1.63,20.83,10.93Z"/>
<path class="cls-1" d="M177.02,256.36c2.46-.32,4.66,3.32,3.08,4.12l-3.08-4.12Z"/>
<path class="cls-6" d="M211.77,249.04c1.6.65,5.23,1.29,5.03,3.38l-1.19,12.44c-.25,2.67-.6,5.14-1.34,7.45-.4,1.25-2.02,2.41-1.74,4.07l2.61,3.32,1.18,21.98,1.27,18.14c.18,2.6-1.32,4.68-3.5,5.32-1,.22-2.14.35-3.18-.19-6.61,1.82-13.27,2.17-20.29,1.35-.95-.11-2.34.63-2.79,1.11-.48.51-.23,1.94-.39,2.85.34,7.53.14,14.75-2.67,21.4-.09.1-.19.28-.26.44l-2.06,4.85-35.37-1.32c-4.34-1.79-6.54-8.04-7.73-7.92s-2.8,6.38-5.48,6.54l-10.41.64c-5.38.33-10.15.72-15.49,2.19-3.14.87-7.62.86-10.83.08-2.95-1.18-3.85-5.14-6.2-6.63"/>
<path class="cls-6" d="M100.69,203.88c4.32.66,9.46,1.37,13.92,1.69l14.97,1.06c3.88.27,7.66-.03,11.21,1.08.19.06.42.03.57.14,2.12,1.41,4.66,1.34,7.15,1.07l19.5-2.14c1.96.84,1.89,4.16,2.43,5.54s1.39,2.32,2.25,3.36l.35.42"/>
<path class="cls-6" d="M100.68,203.82l-.39.4-.78.79-30.19,31.05c.27,1.12,1.58,1.24,2.49,1.36,9.3-2.77,16.87-.34,25.75-.39.82-3.72.22-7.14-.03-10.68-.44-6.17,1.19-12.1,2.27-17.86l.55-2.93.11-.58.22-1.16Z"/>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
<defs>
<style>
.cls-1 {
fill: #0db3c8;
}
.cls-2 {
fill: #007988;
}
.cls-3 {
fill: #0c96a8;
}
.cls-4 {
fill: #3a170d;
}
.cls-5 {
fill: none;
stroke: #381507;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.2px;
}
.cls-6 {
fill: #3c1b0d;
}
</style>
</defs>
<path class="cls-1" d="M214.65,239.1l-2.58-.1c-3.79,2.1-7.73,4.08-11.61,6.32l-6.56,3.79c-2.2,1.27-5.02,2.22-6.12,5.03l12.97-3.01,11.9-1.19c.44-.63.06-1.58.21-2.2,1.11.52,3.9,1.16,3.71,3.1l-1.82,18.71c-1.5,1.84-3.42,2.56-4.11,4.45-.39.04-.84-.28-1.1-.57-.2-.23.18-1.3-.21-1.23l-13.32,2.67-24.66,3.99c2.94.91,5.47.34,8.11.31l30.61-.32c.58,0,1.24.32,1.55.45s.06,1.13-.11,1.49l-5.94,12.1-2.34,5.89c1.31.57,2.15-1.03,3.19-1.23,1.71-.34,2.18,1.87,2.29,2.91.19,1.76-.21,2.53-1.63,3.75l-3.61,3.1c-2.25,1.93-2.51,5.48-3.11,8.25l-2.36,4.83-14.21-1.65c-1.16-.13-2.3.56-2.59,1.21-1.13,2.5,3.49,19.87,1.51,29.66l2.38-.32.4,1.88c-.02,1.11-.76,2.05-1.49,2.86-.9,1-2.37.8-3.88.8l-24.17.07c-3.64.01-6.98.15-10.19-1.01l-.55-.2c.38-.35-.12-1.32,1-2.02l-3.02-10.07c-.55-1.82-.65-4.03-.68-6.05l-.38-23.49c0-.35.21-1.3-.12-1.78-.19-.26,1.36-1.02-.61-1.17l-4.19,32.02c-.53,4.08-2.01,7.72-1.64,11.98,1.56-.87,2.59-.64,3.6-.38-1.06.63-1.9,2.74-3.63,3-8.84,1.34-17.96,1.85-26.77.44l-13.88-3.27c-1.56-1.74-2.19-3.54-3.98-4.71l-6.41,7.6c-5.24-.33-10.27-1.31-15.49-2.21-2.15-4.26-.12-10.16-1.62-14.2-1.64-4.39-1.98-8.67-1.04-13.21,1.9-9.16,1.37-17.8.49-27.02-.43-4.52,3.2-9.56,5.02-12.83l.8-.58.55-.55-.55.55-.8.58c.11.87,1.36,1.26,2.07.83.33-.2.87.26,1.48-.62l30.23-5.39c1.79-.32,6.53.08,5.59-1.14-.12-.15-.41-.79-.78-.81l-16.48-.56-11.54.04c-3.89-.56-7.57.1-11.44-1.87.21,1.09-.13,1.57-.76,1.25-.22-.11-.39.11-.92.03l-2.78-19.95c-.32-2.3-1.51-5.82-.08-7.61l6.94-4.85c.64-.45.02-1.82-.8-1.97-2.71-1.37-4.11-3.91-2.9-7.27-2.45-4.5-2.75-9.25-2.39-14.17l.97-13.21c.38-5.16,5.65-2.67,12.92-.43,3.25.54,6.3-.23,9.33-1.63,8.42.55,16.41-.06,24.8-.67l16.36-1.2,8.59,1.05c.2.02.38.14.56.21-.19-.07-.37-.18-.56-.21-.54,4.1,1.56,7.36,1.72,9.19l1.11,13.13.8,17.66-.2,8.37c.25,0,.57-.14.92.02l3.92-47.24-.57.22.57-.22,19.53-1.91c1.57.6,1.43,3.49,2.12,4.44.09.12.09.4.12.6-.03-.2-.03-.48-.12-.6l-2.15.91c.17,8.51-1.44,13.81-3.15,21.63l-2.13,9.75c2.92-2.92,3.58-6.64,5.27-10.02l10.81-21.63c.85-1.69.49-4.05,3.09-4.69-.5-.7-.42-1.53-.6-2.46,1.54.33,3.13-.5,4.85.26l5.72.4,10.88,1.12,6.13.57c2.16.2,6.59-.53,8.1,2.25.4,6.45,1.14,13.48-.1,19.79l-2.24,11.36ZM168.83,303.16c-6.94.18-12.13-5.11-13.68-11.47-1.72-5.03-2.28-10.32-.88-15.55,1.12-10.54,9.73-25.45,20.19-20.69,5.19,2.36,4.68,11.87,7.33,8.34-.13.18.87-3.46,1.06-3.05-.47-1.02-.68-2.77-.56-2.97.38-.62.92-.03.9.33-.01.16.1.13.26.77.43.68.42,1.72.95,1.95,3.46,1.48,1.8-9.41-6.67-13.59-5.47-2.7-10.86-3.08-15.84-.29-7.33,4.11-11.48,11.04-14.58,18.6-3.51,8.58-5.17,17.76-2.65,26.66,1.55,5.5,3.59,11.18,8.36,15.03,6.06,4.89,14.44,6.2,22.41,3.63,13.87-4.48,18.3-21.76,11.29-15.14-.28.27.49.89.09,1.03-.25.08-.69-.15-1.14.58-.32.51.58,1.26-.3,1.42l-1.48.26,1.93-3.85c.54-1.09.15-2.4-.76-2.62-3.3-.79-4.86,10.32-16.21,10.62ZM99.04,299.48l-2.33-4.68-3.47.03c-.04,6.06,3.08,10.63,7.43,14.01,4.93,3.83,10.3,5.14,16.54,4.87,10.18-.44,19.57-6.79,21.53-16.96,1.57-8.14-2.54-15.62-9.31-19.96-7.16-4.59-16.77-5.53-20.09-10.13-1.83-2.53-1.29-5.65.09-8.51,1.08-2.23,3.67-3.76,6.68-4.4,8.68-1.84,13.61,5.15,15,4.52.46-.2,1.46-.79,1.17-1.39l-1.53-3.17c2.36.38,3.63,3.38,4.81,1.76,1.42-1.94-9.35-13.75-24.73-9.15-8.64,2.58-13.65,10.74-11.83,19.53.96,4.64,3.52,8.84,8.3,10.67l12.22,4.68c6.12,2.34,10.3,8.86,8.33,15.46-1.45,4.86-6.35,7.18-11.25,7.51-5.75.39-12.14-2.11-13.82-7.94-.66-2.3-.6-5.38-4.17-5.51-.75,4.05,1.16,6.89.42,8.75ZM103.94,315l-7.75,6.79c.97.96,1.64.88,2.27.52-.28.16,5.09-5.27,5.48-7.31Z"/>
<path class="cls-3" d="M210.63,274c-.34,2.98,4.2,2.59,4.27,3.76l2.51,42.37c.1,1.67-2.35,3.33-3.58,3.16l.11-2.9c-5.22.96-10.62.62-15.97,0l2.36-4.83c.6-2.77.86-6.32,3.11-8.25l3.61-3.1c1.42-1.22,1.82-1.99,1.63-3.75-.11-1.05-.58-3.25-2.29-2.91-1.04.2-1.88,1.81-3.19,1.23l2.34-5.89,5.94-12.1c.18-.36.42-1.36.11-1.49s-.97-.45-1.55-.45l-30.61.32c-2.64.03-5.17.59-8.11-.31l24.66-3.99,13.32-2.67c.39-.08,0,1,.21,1.23.25.29.71.61,1.1.57Z"/>
<path class="cls-1" d="M213.84,323.29c.69.99.45,1.15-.19,1.78l-17.39,17.12c-3.16,3.11-6.44,5.57-9.4,9.05-.38.45-.97.07-1.39-.06l-.4-1.88c1.94-6.68,3.23-13.66,2.17-20.91-.22-1.52-.28-2.52.82-3.51.9-.81,1.78-.11,3.06-.13l14.27-.23c3.03-.05,5.64-1.6,8.45-1.22Z"/>
<path class="cls-3" d="M69.29,278.12c.52.08.69-.14.92-.03.63.32.97-.16.76-1.25,3.87,1.96,7.55,1.31,11.44,1.87l11.54-.04,16.48.56c.38.01.66.65.78.81.93,1.22-3.8.82-5.59,1.14l-30.23,5.39c-.61.87-1.15.42-1.48.62-.71.43-1.96.04-2.07-.83l.8-.58.55-.55-.55.55.55-.55c.13-.13.21-.35.38-.39.47-.12.74-1.1.12-1.45-.29-.17-.73,0-.96-1.07-.92-1.33-3.2-2.39-3.45-4.18Z"/>
<path class="cls-3" d="M145.75,353.91l-.55-.2c-1.16-.42-2.27-1.18-3.54-1.9-1.2-.68-1.89,1.02-2.51.94-1.01-.26-2.05-.49-3.6.38-.38-4.26,1.1-7.9,1.64-11.98l4.19-32.02c1.97.15.43.91.61,1.17.33.47.11,1.42.12,1.78l.38,23.49c.03,2.02.13,4.23.68,6.05l3.02,10.07c-1.11.7-.61,1.67-1,2.02l.55.2Z"/>
<path class="cls-2" d="M181.3,203.36c.18.94.09,1.76.6,2.46-2.6.64-2.24,3-3.09,4.69l-10.81,21.63c-1.69,3.38-2.35,7.1-5.27,10.02l2.13-9.75c1.71-7.83,3.31-13.12,3.15-21.63l2.15-.91c.09.12.09.4.12.6.19,1.19.46,2.37,1.32,3.3.37-.75,1.52-.99,1.94-1.77l3.72-6.88c.54-1,.67-1.36,1.64-1.89,1.02-.56,1.31-.09,2.4.14Z"/>
<path class="cls-3" d="M147.94,207.55l.57-.22-3.92,47.24c-.34-.16-.67-.01-.92-.02l.2-8.37-.8-17.66-1.11-13.13c-.15-1.82-2.26-5.09-1.72-9.19.2.02.38.14.56.21,2.24.82,4.78.99,7.14,1.13Z"/>
<path class="cls-2" d="M214.65,239.1c-.45,2.26-2.11,3.94-3.91,5.65-.84.16-1.05.39-1.13.79-.15.82.08,1.71,1.03,1.66.76-.04,1.5.2,2.23.54-.16.62.22,1.57-.21,2.2l-11.9,1.19-12.97,3.01c1.1-2.81,3.92-3.76,6.12-5.03l6.56-3.79c3.88-2.24,7.82-4.22,11.61-6.32l2.58.1Z"/>
<path class="cls-4" d="M99.04,299.48c.73-1.86-1.18-4.7-.42-8.75,3.57.13,3.51,3.2,4.17,5.51,1.68,5.83,8.07,8.33,13.82,7.94,4.9-.33,9.79-2.65,11.25-7.51,1.97-6.6-2.21-13.11-8.33-15.46l-12.22-4.68c-4.78-1.83-7.34-6.03-8.3-10.67-1.82-8.79,3.19-16.95,11.83-19.53,15.38-4.6,26.15,7.2,24.73,9.15-1.18,1.62-2.45-1.38-4.81-1.76l1.53,3.17c.29.6-.71,1.18-1.17,1.39-1.39.62-6.32-6.36-15-4.52-3.02.64-5.61,2.16-6.68,4.4-1.38,2.86-1.93,5.98-.09,8.51,3.33,4.59,12.93,5.53,20.09,10.13,6.77,4.34,10.89,11.82,9.31,19.96-1.96,10.18-11.36,16.52-21.53,16.96-6.25.27-11.61-1.04-16.54-4.87-4.35-3.38-7.47-7.95-7.43-14.01l3.47-.03,2.33,4.68Z"/>
<path class="cls-6" d="M168.83,303.16c11.35-.3,12.91-11.42,16.21-10.62.9.22,1.3,1.53.76,2.62l-1.93,3.85,1.48-.26c.88-.16-.02-.91.3-1.42.46-.73.9-.5,1.14-.58.4-.14-.37-.76-.09-1.03,7.01-6.62,2.58,10.66-11.29,15.14-7.97,2.58-16.35,1.27-22.41-3.63-4.76-3.85-6.8-9.53-8.36-15.03-2.51-8.9-.86-18.09,2.65-26.66,3.1-7.56,7.25-14.49,14.58-18.6,4.98-2.79,10.38-2.41,15.84.29,8.47,4.18,10.13,15.07,6.67,13.59-.52-.22-.52-1.27-.95-1.95-.16-.64-.27-.62-.26-.77.02-.36-.52-.95-.9-.33-.12.2.09,1.95.56,2.97-.19-.41-1.19,3.23-1.06,3.05-2.65,3.53-2.14-5.98-7.33-8.34-10.46-4.76-19.07,10.15-20.19,20.69-1.4,5.23-.84,10.52.88,15.55,1.55,6.36,6.74,11.66,13.68,11.47Z"/>
<path class="cls-2" d="M197.98,320.39c5.36.62,10.75.96,15.97,0l-.11,2.9c-2.81-.38-5.42,1.17-8.45,1.22l-14.27.23c-1.27.02-2.16-.67-3.06.13-1.1.99-1.04,1.99-.82,3.51,1.06,7.25-.23,14.24-2.17,20.91l-2.38.32c1.98-9.79-2.64-27.16-1.51-29.66.29-.65,1.43-1.35,2.59-1.21l14.21,1.65Z"/>
<path class="cls-3" d="M103.94,315c-.39,2.04-5.76,7.47-5.48,7.31-.63.36-1.3.44-2.27-.52l7.75-6.79Z"/>
<path class="cls-5" d="M210.64,247.2c.76-.04,1.5.2,2.23.54,1.11.52,3.9,1.16,3.71,3.1l-1.82,18.71c-1.5,1.84-3.42,2.56-4.11,4.45-.34,2.98,4.2,2.59,4.27,3.76l2.51,42.37c.1,1.67-2.35,3.33-3.58,3.16-2.81-.38-5.42,1.17-8.45,1.22l-14.27.23c-1.27.02-2.16-.67-3.06.13-1.1.99-1.04,1.99-.82,3.51,1.06,7.25-.23,14.24-2.17,20.91l.4,1.88c-.02,1.11-.76,2.05-1.49,2.86-.9,1-2.37.8-3.88.8l-24.17.07c-3.64.01-6.98.15-10.19-1.01l-.55-.2c-1.16-.42-2.27-1.18-3.54-1.9-1.2-.68-1.89,1.02-2.51.94-1.06.63-1.9,2.74-3.63,3-8.84,1.34-17.96,1.85-26.77.44l-13.88-3.27c-1.56-1.74-2.19-3.54-3.98-4.71l-6.41,7.6c-5.24-.33-10.27-1.31-15.49-2.21-2.15-4.26-.12-10.16-1.62-14.2-1.64-4.39-1.98-8.67-1.04-13.21,1.9-9.16,1.37-17.8.49-27.02-.43-4.52,3.2-9.56,5.02-12.83l.8-.58.55-.55c.13-.13.21-.35.38-.39"/>
<path class="cls-5" d="M172.5,214.73l-.9-.97c-.85-.92-1.12-2.1-1.32-3.3-.03-.2-.03-.48-.12-.6-.7-.95-.55-3.83-2.12-4.44l-19.53,1.91-.57.22c-2.36-.14-4.9-.31-7.14-1.13-.19-.07-.37-.18-.56-.21l-8.59-1.05-16.36,1.2c-8.38.62-16.38,1.23-24.8.67-3.03,1.4-6.08,2.17-9.33,1.63-7.26-2.24-12.54-4.74-12.92.43l-.97,13.21c-.36,4.92-.07,9.67,2.39,14.17-1.22,3.36.19,5.9,2.9,7.27"/>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -61,11 +61,13 @@
} }
}); });
// Landscape: right clears gear/kit buttons (~4rem); bottom is fixed 60px for // Landscape: right clears gear/kit buttons; bottom is fixed 60px for the
// the kit-bag handle strip — tray is ignored so the stage has room to breathe. // kit-bag handle strip — tray is ignored so the stage has room to breathe.
// At ≥1800px the right sidebar doubles to 8rem so clear 128px.
if (isLandscape) { if (isLandscape) {
rightInset = Math.max(rightInset, 64); // 4rem var xlBreak = vw >= 1800;
bottomInset = 60; // kit-bag handle rightInset = Math.max(rightInset, xlBreak ? 128 : 64);
bottomInset = 60;
} }
overlay.style.paddingRight = rightInset + 'px'; overlay.style.paddingRight = rightInset + 'px';
@@ -78,7 +80,11 @@
var sw = stageEl.offsetWidth - 24; // subtract padding (0.75rem × 2) var sw = stageEl.offsetWidth - 24; // subtract padding (0.75rem × 2)
var sh = stageEl.offsetHeight - 24; var sh = stageEl.offsetHeight - 24;
if (sw > 0 && sh > 0) { if (sw > 0 && sh > 0) {
var cardW = Math.min(sw * 0.4, sh * 0.8 * 5 / 8); // Clamp between 90px (never tiny in landscape) and 160px (never
// dominant on very wide/tall viewports). In portrait, skip the
// floor so small modals still scale down naturally.
var cardW = Math.min(sw * 0.4, sh * 0.8 * 5 / 8, 160);
if (isLandscape) { cardW = Math.max(cardW, 90); }
overlay.style.setProperty('--sig-card-w', cardW + 'px'); overlay.style.setProperty('--sig-card-w', cardW + 'px');
} }
} }

View File

@@ -5,14 +5,23 @@ var SigSelect = (function () {
gravity: ['BC', 'EC', 'AC'], gravity: ['BC', 'EC', 'AC'],
}; };
var overlay, deckGrid, stage, stageCard; var overlay, deckGrid, stage, stageCard, statBlock;
var cautionEl, cautionEffect, cautionPrev, cautionNext, cautionIndexEl;
var _flipBtn, _cautionBtn, _flipOrigLabel, _cautionOrigLabel;
var reserveUrl, userRole, userPolarity; var reserveUrl, userRole, userPolarity;
var _cautionData = [];
var _cautionIdx = 0;
var _focusedCardEl = null; // card currently shown in stage var _focusedCardEl = null; // card currently shown in stage
var _reservedCardId = null; // card with active reservation var _reservedCardId = null; // card with active reservation
var _stageFrozen = false; // true after OK — stage locks on reserved card var _stageFrozen = false; // true after OK — stage locks on reserved card
var _requestInFlight = false; var _requestInFlight = false;
var _floatingCursors = {}; // key: cardId+posClass → portal <i> element (hover)
var _reservedFloats = {}; // key: role → portal <i> element (thumbs-up, frozen)
var _cursorPortal = null;
function getCsrf() { function getCsrf() {
var m = document.cookie.match(/csrftoken=([^;]+)/); var m = document.cookie.match(/csrftoken=([^;]+)/);
return m ? m[1] : ''; return m ? m[1] : '';
@@ -20,8 +29,60 @@ var SigSelect = (function () {
// ── Stage ────────────────────────────────────────────────────────────── // ── Stage ──────────────────────────────────────────────────────────────
function _populateKeywordList(listEl, csv) {
var keywords = csv ? csv.split(',').filter(Boolean) : [];
listEl.innerHTML = keywords.map(function (k) {
return '<li>' + k.trim() + '</li>';
}).join('');
}
// ── Caution tooltip ───────────────────────────────────────────────────
function _renderCaution() {
if (_cautionData.length === 0) {
cautionEffect.innerHTML = '<em>Rival interactions pending.</em>';
cautionPrev.disabled = true;
cautionNext.disabled = true;
cautionIndexEl.textContent = '';
return;
}
cautionEffect.innerHTML = _cautionData[_cautionIdx];
cautionPrev.disabled = (_cautionData.length <= 1);
cautionNext.disabled = (_cautionData.length <= 1);
cautionIndexEl.textContent = _cautionData.length > 1
? (_cautionIdx + 1) + ' / ' + _cautionData.length
: '';
}
function _openCaution() {
if (!_focusedCardEl) return;
try {
_cautionData = JSON.parse(_focusedCardEl.dataset.cautions || '[]');
} catch (e) {
_cautionData = [];
}
_cautionIdx = 0;
_renderCaution();
_flipBtn.classList.add('btn-disabled');
_cautionBtn.classList.add('btn-disabled');
_flipBtn.textContent = '\u00D7';
_cautionBtn.textContent = '\u00D7';
stage.classList.add('sig-caution-open');
}
function _closeCaution() {
stage.classList.remove('sig-caution-open');
if (_flipBtn) {
_flipBtn.classList.remove('btn-disabled');
_cautionBtn.classList.remove('btn-disabled');
_flipBtn.textContent = _flipOrigLabel;
_cautionBtn.textContent = _cautionOrigLabel;
}
}
function updateStage(cardEl) { function updateStage(cardEl) {
if (_stageFrozen) return; if (_stageFrozen) return;
_closeCaution();
if (!cardEl) { if (!cardEl) {
stageCard.style.display = 'none'; stageCard.style.display = 'none';
stage.classList.remove('sig-stage--active'); stage.classList.remove('sig-stage--active');
@@ -47,9 +108,26 @@ var SigSelect = (function () {
} }
}); });
stageCard.querySelector('.fan-card-name-group').textContent = group; stageCard.querySelector('.fan-card-name-group').textContent = group;
stageCard.querySelector('.fan-card-name').textContent = title;
stageCard.querySelector('.fan-card-arcana').textContent = arcana; stageCard.querySelector('.fan-card-arcana').textContent = arcana;
stageCard.querySelector('.fan-card-correspondence').textContent = corr; stageCard.querySelector('.fan-card-correspondence').textContent = ''; // shown in game-kit only
var qualifier = userPolarity === 'levity' ? 'Leavened' : 'Graven';
var isMajor = arcana.toLowerCase().indexOf('major') !== -1;
// Major arcana: qualifier sits below the title — append comma so it reads as a subtitle.
stageCard.querySelector('.fan-card-name').textContent = isMajor ? title + ',' : title;
stageCard.querySelector('.sig-qualifier-above').textContent = isMajor ? '' : qualifier;
stageCard.querySelector('.sig-qualifier-below').textContent = isMajor ? qualifier : '';
// Populate stat block keyword faces and reset to upright
statBlock.classList.remove('is-reversed');
_populateKeywordList(
statBlock.querySelector('#id_stat_keywords_upright'),
cardEl.dataset.keywordsUpright
);
_populateKeywordList(
statBlock.querySelector('#id_stat_keywords_reversed'),
cardEl.dataset.keywordsReversed
);
stageCard.style.display = ''; stageCard.style.display = '';
stage.classList.add('sig-stage--active'); stage.classList.add('sig-stage--active');
@@ -110,6 +188,38 @@ var SigSelect = (function () {
// ── Apply reservation state (local + from WS) ───────────────────────── // ── Apply reservation state (local + from WS) ─────────────────────────
function _placeReservedFloat(cardId, cardEl, role) {
// Remove any pre-existing reserved float for this role (e.g. page-load replay)
if (_reservedFloats[role]) { _reservedFloats[role].remove(); }
// Retire ALL hover floats for this role — may be on a different card than reserved
var roles = POLARITY_ROLES[userPolarity] || [];
var idx = roles.indexOf(role);
var posClass = ['--left', '--mid', '--right'][idx] || '--left';
Object.keys(_floatingCursors).forEach(function (key) {
if (key.slice(-posClass.length) === posClass) {
_floatingCursors[key].remove();
var hCid = key.slice(0, key.length - posClass.length);
var hEl = deckGrid.querySelector('.sig-card[data-card-id="' + hCid + '"]');
if (hEl) {
var a = hEl.querySelector('.sig-cursor' + posClass);
if (a) a.classList.remove('active');
}
delete _floatingCursors[key];
}
});
var rect = cardEl.getBoundingClientRect();
var xFractions = [0.15, 0.5, 0.85];
var fc = document.createElement('i');
fc.className = 'fa-solid fa-thumbs-up sig-cursor-float sig-cursor-float--reserved';
fc.dataset.role = role;
fc.style.left = (rect.left + rect.width * xFractions[idx < 0 ? 1 : idx]) + 'px';
fc.style.top = rect.bottom + 'px';
_ensureCursorPortal().appendChild(fc);
_reservedFloats[role] = fc;
}
function applyReservation(cardId, role, reserved) { function applyReservation(cardId, role, reserved) {
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]'); var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
if (!cardEl) return; if (!cardEl) return;
@@ -127,6 +237,8 @@ var SigSelect = (function () {
_stageFrozen = true; _stageFrozen = true;
stage.classList.add('sig-stage--frozen'); stage.classList.add('sig-stage--frozen');
} }
// Thumbs-up float for all reservations — own role sees their own indicator too
_placeReservedFloat(cardId, cardEl, role);
} else { } else {
delete cardEl.dataset.reservedBy; delete cardEl.dataset.reservedBy;
cardEl.classList.remove('sig-reserved', 'sig-reserved--own'); cardEl.classList.remove('sig-reserved', 'sig-reserved--own');
@@ -135,26 +247,65 @@ var SigSelect = (function () {
_stageFrozen = false; _stageFrozen = false;
stage.classList.remove('sig-stage--frozen'); stage.classList.remove('sig-stage--frozen');
} }
// Remove thumbs-up float for all releases — own role included
if (_reservedFloats[role]) {
_reservedFloats[role].remove();
delete _reservedFloats[role];
}
} }
} }
// ── Apply hover cursor (WS only — own hover is CSS :hover) ──────────── // ── Apply hover cursor (WS only — own hover is CSS :hover) ────────────
//
// Cursor icons are portaled to document root so they escape overflow/clip
// contexts in the deck grid. The in-card anchor elements only carry the
// .active class (for test assertions and the :has() z-index rule).
function _ensureCursorPortal() {
if (!_cursorPortal || !document.body.contains(_cursorPortal)) {
_cursorPortal = document.getElementById('id_sig_cursor_portal');
if (!_cursorPortal) {
_cursorPortal = document.createElement('div');
_cursorPortal.id = 'id_sig_cursor_portal';
document.body.appendChild(_cursorPortal);
}
}
return _cursorPortal;
}
function applyHover(cardId, role, active) { function applyHover(cardId, role, active) {
if (role === userRole) return; if (role === userRole) return;
if (_reservedFloats[role]) return; // role already has thumbs-up — ignore hover
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]'); var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
if (!cardEl) return; if (!cardEl) return;
var roles = POLARITY_ROLES[userPolarity] || []; var roles = POLARITY_ROLES[userPolarity] || [];
var idx = roles.indexOf(role); var idx = roles.indexOf(role);
var posClass = ['--left', '--mid', '--right'][idx] || '--left'; var posClass = ['--left', '--mid', '--right'][idx] || '--left';
var cursor = cardEl.querySelector('.sig-cursor' + posClass); var anchor = cardEl.querySelector('.sig-cursor' + posClass);
if (!cursor) return; if (!anchor) return;
var key = cardId + posClass;
if (active) { if (active) {
cursor.classList.add('active'); anchor.classList.add('active'); // kept for test assertions + :has() z-index
// Place a fixed-position clone in the portal, positioned from card bounds
var rect = cardEl.getBoundingClientRect();
var xFractions = [0.15, 0.5, 0.85];
var fc = document.createElement('i');
fc.className = 'fa-solid fa-hand-pointer sig-cursor-float';
fc.dataset.role = role;
fc.style.left = (rect.left + rect.width * xFractions[idx]) + 'px';
fc.style.top = rect.bottom + 'px';
_ensureCursorPortal().appendChild(fc);
_floatingCursors[key] = fc;
} else { } else {
cursor.classList.remove('active'); anchor.classList.remove('active');
if (_floatingCursors[key]) {
_floatingCursors[key].remove();
delete _floatingCursors[key];
}
} }
} }
@@ -188,17 +339,64 @@ var SigSelect = (function () {
deckGrid = overlay.querySelector('.sig-deck-grid'); deckGrid = overlay.querySelector('.sig-deck-grid');
stage = overlay.querySelector('.sig-stage'); stage = overlay.querySelector('.sig-stage');
stageCard = stage.querySelector('.sig-stage-card'); stageCard = stage.querySelector('.sig-stage-card');
statBlock = stage.querySelector('.sig-stat-block');
_flipBtn = statBlock.querySelector('.sig-flip-btn');
_cautionBtn = statBlock.querySelector('.sig-caution-btn');
_flipOrigLabel = _flipBtn.textContent;
_cautionOrigLabel = _cautionBtn.textContent;
_flipBtn.addEventListener('click', function () {
if (_flipBtn.classList.contains('btn-disabled')) return;
statBlock.classList.toggle('is-reversed');
});
cautionEl = stage.querySelector('.sig-caution-tooltip');
cautionEffect = cautionEl.querySelector('.sig-caution-effect');
cautionPrev = statBlock.querySelector('.sig-caution-prev');
cautionNext = statBlock.querySelector('.sig-caution-next');
cautionIndexEl = cautionEl.querySelector('.sig-caution-index');
// Clicking the tooltip (not nav buttons) dismisses it
cautionEl.addEventListener('click', function () {
_closeCaution();
});
_cautionBtn.addEventListener('click', function () {
if (_cautionBtn.classList.contains('btn-disabled')) return;
stage.classList.contains('sig-caution-open') ? _closeCaution() : _openCaution();
});
cautionPrev.addEventListener('click', function () {
_cautionIdx = (_cautionIdx - 1 + _cautionData.length) % _cautionData.length;
_renderCaution();
});
cautionNext.addEventListener('click', function () {
_cautionIdx = (_cautionIdx + 1) % _cautionData.length;
_renderCaution();
});
reserveUrl = overlay.dataset.reserveUrl; reserveUrl = overlay.dataset.reserveUrl;
userRole = overlay.dataset.userRole; userRole = overlay.dataset.userRole;
userPolarity= overlay.dataset.polarity; userPolarity= overlay.dataset.polarity;
// Restore reservations from server-rendered JSON (page-load state) // Restore reservations from server-rendered JSON (page-load state).
// Deferred to 'load' so sizeSigModal() (also a 'load' listener, registered
// in room.js before this script) has already applied paddingBottom and
// --sig-card-w before _placeReservedFloat calls getBoundingClientRect().
try { try {
var existing = JSON.parse(overlay.dataset.reservations || '{}'); var existing = JSON.parse(overlay.dataset.reservations || '{}');
Object.keys(existing).forEach(function (cardId) { if (Object.keys(existing).length) {
applyReservation(cardId, existing[cardId], true); var _replayReservations = function () {
}); Object.keys(existing).forEach(function (cardId) {
applyReservation(cardId, existing[cardId], true);
});
};
if (document.readyState === 'complete') {
_replayReservations();
} else {
window.addEventListener('load', _replayReservations, { once: true });
}
}
} catch (e) { /* malformed JSON — ignore */ } } catch (e) { /* malformed JSON — ignore */ }
// Hover: update stage preview + broadcast cursor // Hover: update stage preview + broadcast cursor
@@ -264,6 +462,13 @@ var SigSelect = (function () {
_reservedCardId = null; _reservedCardId = null;
_stageFrozen = false; _stageFrozen = false;
_requestInFlight = false; _requestInFlight = false;
_cautionData = [];
_cautionIdx = 0;
Object.keys(_floatingCursors).forEach(function (k) { _floatingCursors[k].remove(); });
_floatingCursors = {};
Object.keys(_reservedFloats).forEach(function (k) { _reservedFloats[k].remove(); });
_reservedFloats = {};
_cursorPortal = null;
init(); init();
}, },
_setFrozen: function (v) { _stageFrozen = v; }, _setFrozen: function (v) { _stageFrozen = v; },

View File

@@ -14,6 +14,13 @@ var Tray = (function () {
var _tray = null; var _tray = null;
var _grid = null; var _grid = null;
// Role code → scrawl SVG name mapping for tray card display.
var _ROLE_SCRAWL = {
PC: 'Player', NC: 'Narrator', EC: 'Economist',
SC: 'Shepherd', AC: 'Alchemist', BC: 'Builder'
};
var _roleIconsUrl = null;
// Portrait bounds (X axis) // Portrait bounds (X axis)
var _minLeft = 0; var _minLeft = 0;
var _maxLeft = 0; var _maxLeft = 0;
@@ -94,12 +101,7 @@ var Tray = (function () {
// Closed: tray hidden above viewport, handle visible at y=0. // Closed: tray hidden above viewport, handle visible at y=0.
_maxTop = -(gearBtnTop - handleH); _maxTop = -(gearBtnTop - handleH);
} else { } else {
// Portrait: slide on X axis. // Portrait: wrap width = full viewport; handle parks at right edge.
// Wrap width is pinned to viewportW (JS) so its right edge only
// reaches the viewport boundary when left = 0 (fully open).
// This mirrors landscape: the open edge appears only at the last moment.
// Open: left = 0 → wrap right = viewportW exactly.
// Closed: left = viewportW - handleW → tray fully off-screen right.
var handleW = _btn.offsetWidth || 48; var handleW = _btn.offsetWidth || 48;
if (_wrap) _wrap.style.width = window.innerWidth + 'px'; if (_wrap) _wrap.style.width = window.innerWidth + 'px';
_minLeft = 0; _minLeft = 0;
@@ -251,7 +253,13 @@ var Tray = (function () {
firstCell.classList.add('tray-role-card'); firstCell.classList.add('tray-role-card');
firstCell.dataset.role = roleCode; firstCell.dataset.role = roleCode;
firstCell.textContent = roleCode; firstCell.textContent = '';
if (_roleIconsUrl) {
var img = document.createElement('img');
img.src = _roleIconsUrl + 'starter-role-' + (_ROLE_SCRAWL[roleCode] || 'Blank') + '.svg';
img.alt = roleCode;
firstCell.appendChild(img);
}
open(); open();
_arcIn(firstCell, function () { _arcIn(firstCell, function () {
@@ -313,7 +321,7 @@ var Tray = (function () {
} }
} else { } else {
if (_tray) _tray.style.display = 'none'; if (_tray) _tray.style.display = 'none';
if (_wrap) { _wrap.style.top = ''; _wrap.style.height = ''; } if (_wrap) { _wrap.style.top = ''; _wrap.style.height = ''; _wrap.style.width = ''; }
_computeBounds(); _computeBounds();
_applyVerticalBounds(); _applyVerticalBounds();
_computeCellSize(); _computeCellSize();
@@ -331,6 +339,7 @@ var Tray = (function () {
_btn = document.getElementById('id_tray_btn'); _btn = document.getElementById('id_tray_btn');
_tray = document.getElementById('id_tray'); _tray = document.getElementById('id_tray');
_grid = document.getElementById('id_tray_grid'); _grid = document.getElementById('id_tray_grid');
_roleIconsUrl = (_grid && _grid.dataset.roleIconsUrl) || null;
if (!_btn) return; if (!_btn) return;
if (_isLandscape()) { if (_isLandscape()) {
@@ -342,8 +351,8 @@ var Tray = (function () {
if (_wrap) _wrap.style.top = _maxTop + 'px'; if (_wrap) _wrap.style.top = _maxTop + 'px';
_computeCellSize(); _computeCellSize();
} else { } else {
// Clear landscape's inline top so portrait CSS applies. // Clear landscape's inline top/height/width so portrait CSS applies.
if (_wrap) _wrap.style.top = ''; if (_wrap) { _wrap.style.top = ''; _wrap.style.width = ''; }
_applyVerticalBounds(); _applyVerticalBounds();
_computeCellSize(); // wrap has correct height after _applyVerticalBounds _computeCellSize(); // wrap has correct height after _applyVerticalBounds
_computeBounds(); _computeBounds();

View File

@@ -269,16 +269,16 @@ class SigDeckCompositionTest(TestCase):
cards = sig_deck_cards(self.room) cards = sig_deck_cards(self.room)
self.assertEqual(len(cards), 36) self.assertEqual(len(cards), 36)
def test_sc_ac_contribute_court_cards_of_swords_and_cups(self): def test_sc_ac_contribute_court_cards_of_blades_and_grails(self):
cards = sig_deck_cards(self.room) cards = sig_deck_cards(self.room)
sc_ac = [c for c in cards if c.suit in ("SWORDS", "CUPS")] sc_ac = [c for c in cards if c.suit in ("BLADES", "GRAILS")]
# M/J/Q/K × 2 suits × 2 roles = 16 # M/J/Q/K × 2 suits × 2 roles = 16
self.assertEqual(len(sc_ac), 16) self.assertEqual(len(sc_ac), 16)
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in sc_ac)) self.assertTrue(all(c.number in (11, 12, 13, 14) for c in sc_ac))
def test_pc_bc_contribute_court_cards_of_wands_and_crowns(self): def test_pc_bc_contribute_court_cards_of_brands_and_crowns(self):
cards = sig_deck_cards(self.room) cards = sig_deck_cards(self.room)
pc_bc = [c for c in cards if c.suit in ("WANDS", "CROWNS")] pc_bc = [c for c in cards if c.suit in ("BRANDS", "CROWNS")]
self.assertEqual(len(pc_bc), 16) self.assertEqual(len(pc_bc), 16)
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in pc_bc)) self.assertTrue(all(c.number in (11, 12, 13, 14) for c in pc_bc))
@@ -342,7 +342,7 @@ class SigCardFieldTest(TestCase):
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
) )
self.card = TarotCard.objects.get( self.card = TarotCard.objects.get(
deck_variant=earthman, arcana="MIDDLE", suit="WANDS", number=11, deck_variant=earthman, arcana="MIDDLE", suit="BRANDS", number=11,
) )
owner = User.objects.create(email="owner@test.io") owner = User.objects.create(email="owner@test.io")
room = Room.objects.create(name="Field Test", owner=owner) room = Room.objects.create(name="Field Test", owner=owner)
@@ -479,3 +479,54 @@ class SigCardHelperTest(TestCase):
self.owner.save() self.owner.save()
self.assertEqual(levity_sig_cards(self.room), []) self.assertEqual(levity_sig_cards(self.room), [])
self.assertEqual(gravity_sig_cards(self.room), []) self.assertEqual(gravity_sig_cards(self.room), [])
class TarotCardCautionsTest(TestCase):
"""TarotCard.cautions JSONField — field existence and Schizo seed data."""
def setUp(self):
self.earthman = DeckVariant.objects.get(slug="earthman")
def test_cautions_field_saves_and_retrieves_list(self):
card = TarotCard.objects.create(
deck_variant=self.earthman,
arcana="MINOR",
suit="CROWNS",
number=99,
name="Test Card",
slug="test-card-cautions",
cautions=["First caution.", "Second caution."],
)
card.refresh_from_db()
self.assertEqual(card.cautions, ["First caution.", "Second caution."])
def test_cautions_defaults_to_empty_list(self):
card = TarotCard.objects.create(
deck_variant=self.earthman,
arcana="MINOR",
suit="CROWNS",
number=98,
name="Default Cautions Card",
slug="default-cautions-card",
)
self.assertEqual(card.cautions, [])
def test_schizo_has_4_cautions(self):
schizo = TarotCard.objects.get(
deck_variant=self.earthman, arcana="MAJOR", number=1
)
self.assertEqual(len(schizo.cautions), 4)
def test_schizo_caution_references_the_pervert(self):
schizo = TarotCard.objects.get(
deck_variant=self.earthman, arcana="MAJOR", number=1
)
self.assertIn("The Pervert", schizo.cautions[0])
def test_schizo_cautions_use_reverse_language(self):
schizo = TarotCard.objects.get(
deck_variant=self.earthman, arcana="MAJOR", number=1
)
for caution in schizo.cautions:
self.assertIn("reverse", caution)
self.assertNotIn("transform", caution)

View File

@@ -926,7 +926,7 @@ def _full_sig_setUp(test_case, role_order=None):
room.table_status = Room.SIG_SELECT room.table_status = Room.SIG_SELECT
room.save() room.save()
card_in_deck = TarotCard.objects.get( card_in_deck = TarotCard.objects.get(
deck_variant=earthman, arcana="MIDDLE", suit="WANDS", number=11 deck_variant=earthman, arcana="MIDDLE", suit="BRANDS", number=11
) )
test_case.client.force_login(founder) test_case.client.force_login(founder)
return room, gamers, earthman, card_in_deck return room, gamers, earthman, card_in_deck
@@ -963,6 +963,32 @@ class SigSelectRenderingTest(TestCase):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertNotContains(response, "id_sig_deck") self.assertNotContains(response, "id_sig_deck")
def test_sig_cards_render_keyword_data_attributes(self):
response = self.client.get(self.url)
content = response.content.decode()
self.assertIn("data-keywords-upright=", content)
self.assertIn("data-keywords-reversed=", content)
def test_sig_stat_block_structure_rendered(self):
response = self.client.get(self.url)
self.assertContains(response, "sig-stat-block")
self.assertContains(response, "sig-flip-btn")
self.assertContains(response, "stat-face--upright")
self.assertContains(response, "stat-face--reversed")
def test_sig_cards_render_cautions_data_attribute(self):
response = self.client.get(self.url)
self.assertContains(response, "data-cautions=")
def test_sig_caution_tooltip_structure_rendered(self):
response = self.client.get(self.url)
self.assertContains(response, "sig-caution-tooltip")
self.assertContains(response, "sig-caution-btn")
self.assertContains(response, "sig-caution-effect")
self.assertContains(response, "sig-caution-index")
self.assertContains(response, "sig-caution-prev")
self.assertContains(response, "sig-caution-next")
class SelectSigCardViewTest(TestCase): class SelectSigCardViewTest(TestCase):
"""select_sig view — records choice, enforces turn order, rejects bad input.""" """select_sig view — records choice, enforces turn order, rejects bad input."""
@@ -1000,8 +1026,8 @@ class SelectSigCardViewTest(TestCase):
def test_select_sig_card_not_in_deck_returns_400(self): def test_select_sig_card_not_in_deck_returns_400(self):
# Create a pip card (number=5) — not in the sig deck (only court 1114 + major 01) # Create a pip card (number=5) — not in the sig deck (only court 1114 + major 01)
other = TarotCard.objects.create( other = TarotCard.objects.create(
deck_variant=self.earthman, arcana="MINOR", suit="WANDS", number=5, deck_variant=self.earthman, arcana="MINOR", suit="BRANDS", number=5,
name="Five of Wands Test", slug="five-of-wands-test", name="Five of Brands Test", slug="five-of-brands-test",
keywords_upright=[], keywords_reversed=[], keywords_upright=[], keywords_reversed=[],
) )
response = self._post(card_id=other.id) response = self._post(card_id=other.id)
@@ -1188,7 +1214,7 @@ class SigReserveViewTest(TestCase):
def test_reserve_different_card_while_holding_returns_409(self): def test_reserve_different_card_while_holding_returns_409(self):
"""Cannot OK a different card while holding one — must NVM first.""" """Cannot OK a different card while holding one — must NVM first."""
card_b = TarotCard.objects.filter( card_b = TarotCard.objects.filter(
deck_variant=self.earthman, arcana="MIDDLE", suit="WANDS", number=12 deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=12
).first() ).first()
self._reserve() # PC grabs card A → 200 self._reserve() # PC grabs card A → 200
response = self._reserve(card_id=card_b.id) # tries card B → 409 response = self._reserve(card_id=card_b.id) # tries card B → 409
@@ -1210,7 +1236,7 @@ class SigReserveViewTest(TestCase):
def test_reserve_blocked_then_unblocked_after_release(self): def test_reserve_blocked_then_unblocked_after_release(self):
"""After NVM, a new card can be OK'd.""" """After NVM, a new card can be OK'd."""
card_b = TarotCard.objects.filter( card_b = TarotCard.objects.filter(
deck_variant=self.earthman, arcana="MIDDLE", suit="WANDS", number=12 deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=12
).first() ).first()
self._reserve() # hold card A self._reserve() # hold card A
self._reserve(action="release") # NVM self._reserve(action="release") # NVM

View File

@@ -10,8 +10,11 @@ from django.shortcuts import redirect, render
from django.utils import timezone from django.utils import timezone
from apps.drama.models import GameEvent, record from apps.drama.models import GameEvent, record
from django.db.models import Case, IntegerField, Value, When
from apps.epic.models import ( from apps.epic.models import (
GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard, TarotDeck, GateSlot, Room, RoomInvite, SIG_SEAT_ORDER, SigReservation, TableSeat,
TarotCard, TarotDeck,
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards, active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
select_token, sig_deck_cards, select_token, sig_deck_cards,
) )
@@ -92,6 +95,27 @@ def _notify_sig_reserved(room_id, card_id, role, reserved):
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"} SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
_SIG_SEAT_ORDERING = Case(
*[When(role=r, then=Value(i)) for i, r in enumerate(SIG_SEAT_ORDER)],
default=Value(99),
output_field=IntegerField(),
)
def _canonical_user_seat(room, user):
"""Return the user's seat whose role comes first in PC→NC→EC→SC→AC→BC order.
In normal play (one user = one seat) this is equivalent to .first().
For Carte Blanche (one user = all seats) it returns the PC seat, ensuring
sig-select cursor placement is seat-based, not position/slot-based.
"""
return room.table_seats.filter(gamer=user).order_by(_SIG_SEAT_ORDERING).first()
_ROLE_SCRAWL_NAMES = {
"PC": "Player", "NC": "Narrator", "EC": "Economist",
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
}
def _gate_positions(room): def _gate_positions(room):
"""Return list of dicts [{slot, role_label, role_assigned}] for _table_positions.html.""" """Return list of dicts [{slot, role_label, role_assigned}] for _table_positions.html."""
@@ -216,10 +240,16 @@ def _role_select_context(room, user):
if user.is_authenticated else [] if user.is_authenticated else []
) )
active_slot = active_seat.slot_number if active_seat else None active_slot = active_seat.slot_number if active_seat else None
_my_role = assigned_seats[0].role if assigned_seats else None
ctx = { ctx = {
"card_stack_state": card_stack_state, "card_stack_state": card_stack_state,
"starter_roles": starter_roles, "starter_roles": starter_roles,
"assigned_seats": assigned_seats, "assigned_seats": assigned_seats,
"my_tray_role": _my_role,
"my_tray_scrawl_static_path": (
f"apps/epic/icons/cards-roles/starter-role-{_ROLE_SCRAWL_NAMES[_my_role]}.svg"
if _my_role else None
),
"user_seat": user_seat, "user_seat": user_seat,
"user_slots": list( "user_slots": list(
room.table_seats.filter(gamer=user, role__isnull=True) room.table_seats.filter(gamer=user, role__isnull=True)
@@ -231,7 +261,7 @@ def _role_select_context(room, user):
"slots": room.gate_slots.order_by("slot_number"), "slots": room.gate_slots.order_by("slot_number"),
} }
if room.table_status == Room.SIG_SELECT: if room.table_status == Room.SIG_SELECT:
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
user_role = user_seat.role if user_seat else None user_role = user_seat.role if user_seat else None
user_polarity = None user_polarity = None
if user_role in _LEVITY_ROLES: if user_role in _LEVITY_ROLES:
@@ -572,7 +602,7 @@ def sig_reserve(request, room_id):
if room.table_status != Room.SIG_SELECT: if room.table_status != Room.SIG_SELECT:
return HttpResponse(status=400) return HttpResponse(status=400)
user_seat = room.table_seats.filter(gamer=request.user).first() user_seat = _canonical_user_seat(room, request.user)
if not user_seat or not user_seat.role: if not user_seat or not user_seat.role:
return HttpResponse(status=403) return HttpResponse(status=403)
@@ -611,7 +641,7 @@ def sig_reserve(request, room_id):
SigReservation.objects.create( SigReservation.objects.create(
room=room, gamer=request.user, card=card, room=room, gamer=request.user, card=card,
role=user_seat.role, polarity=polarity, seat=user_seat, role=user_seat.role, polarity=polarity,
) )
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True) _notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
return HttpResponse(status=200) return HttpResponse(status=200)

View File

@@ -5,6 +5,7 @@ from . import views as lyric_views
urlpatterns = [ urlpatterns = [
path('send_login_email', lyric_views.send_login_email, name='send_login_email'), path('send_login_email', lyric_views.send_login_email, name='send_login_email'),
path('login', lyric_views.login, name='login'), path('login', lyric_views.login, name='login'),
path('logout', auth_views.LogoutView.as_view(next_page='/'), name='logout') path('logout', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
path('dev-login/<str:session_key>/', lyric_views.dev_login, name='dev_login'),
] ]

View File

@@ -1,4 +1,6 @@
from django.conf import settings
from django.contrib import auth, messages from django.contrib import auth, messages
from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
@@ -27,3 +29,13 @@ def login(request):
else: else:
messages.error(request, "Invalid login link!—please request another") messages.error(request, "Invalid login link!—please request another")
return redirect("/") return redirect("/")
def dev_login(request, session_key):
"""DEBUG-only: set session cookie and redirect. Used by setup_sig_session command."""
if not settings.DEBUG:
raise Http404
next_url = request.GET.get("next", "/")
response = redirect(next_url)
response.set_cookie(settings.SESSION_COOKIE_NAME, session_key, httponly=True)
return response

View File

@@ -57,13 +57,13 @@ INSTALLED_APPS = [
# Board apps # Board apps
'apps.dashboard', 'apps.dashboard',
'apps.gameboard', 'apps.gameboard',
'apps.billboard',
# Gamer apps # Gamer apps
'apps.lyric', 'apps.lyric',
'apps.epic', 'apps.epic',
'apps.drama', 'apps.drama',
'apps.billboard',
'apps.ap',
# Custom apps # Custom apps
'apps.ap',
'apps.api', 'apps.api',
'apps.applets', 'apps.applets',
'functional_tests', 'functional_tests',

View File

@@ -0,0 +1,128 @@
"""
Management command for manual multi-user sig-select testing.
Creates (or reuses) a room with all 6 gate slots filled, roles assigned,
and table_status=SIG_SELECT. Prints one pre-auth URL per gamer so you can
paste them into 6 Firefox Multi-Account Container tabs.
Usage:
python src/manage.py setup_sig_session
python src/manage.py setup_sig_session --base-url http://localhost:8000
python src/manage.py setup_sig_session --room <uuid> # reuse existing room
"""
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
from django.contrib.sessions.backends.db import SessionStore
from django.core.management.base import BaseCommand
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat, TarotCard
from apps.lyric.models import User
GAMERS = [
("founder@test.io", "discoman"),
("amigo@test.io", "amigo"),
("bud@test.io", "bud"),
("pal@test.io", "pal"),
("dude@test.io", "dude"),
("bro@test.io", "bro"),
]
ROLES = ["PC", "NC", "EC", "SC", "AC", "BC"]
def _ensure_earthman():
"""Return (or create) the Earthman DeckVariant with enough sig-deck cards seeded."""
earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"):
for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create(
deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={
"arcana": "MINOR",
"suit": suit,
"number": number,
"name": f"{_NAME[number]} of {suit.capitalize()}",
},
)
return earthman
def _make_session(user):
session = SessionStore()
session[SESSION_KEY] = str(user.pk)
session[BACKEND_SESSION_KEY] = "apps.lyric.authentication.PasswordlessAuthenticationBackend"
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
session.save()
return session.session_key
class Command(BaseCommand):
help = "Set up a SIG_SELECT room and print pre-auth URLs for all six gamers"
def add_arguments(self, parser):
parser.add_argument("--base-url", default="http://localhost:8000")
parser.add_argument("--room", default=None, help="UUID of an existing room to reuse")
def handle(self, *args, **options):
base_url = options["base_url"].rstrip("/")
earthman = _ensure_earthman()
# ── Users ────────────────────────────────────────────────────────────
users = []
for email, _ in GAMERS:
user, _ = User.objects.get_or_create(email=email)
user.is_staff = True
user.is_superuser = True
if not user.equipped_deck:
user.equipped_deck = earthman
user.save()
users.append(user)
# ── Room ─────────────────────────────────────────────────────────────
if options["room"]:
room = Room.objects.get(pk=options["room"])
else:
room = Room.objects.create(
name="Sig Select Test Room",
owner=users[0],
visibility=Room.PUBLIC,
)
# ── Gate slots ───────────────────────────────────────────────────────
for i, user in enumerate(users, start=1):
slot = room.gate_slots.get(slot_number=i)
slot.gamer = user
slot.status = GateSlot.FILLED
slot.save()
room.gate_status = Room.OPEN
room.save()
# ── Table seats + roles ──────────────────────────────────────────────
for i, (user, role) in enumerate(zip(users, ROLES), start=1):
TableSeat.objects.update_or_create(
room=room, slot_number=i,
defaults={"gamer": user, "role": role, "role_revealed": True},
)
room.table_status = Room.SIG_SELECT
room.save()
# ── Print URLs ───────────────────────────────────────────────────────
room_path = f"/gameboard/room/{room.pk}/"
self.stdout.write(f"\nRoom: {base_url}{room_path}\n")
self.stdout.write(f"{'Container':<12} {'Email':<22} {'Role':<6} URL")
self.stdout.write("" * 100)
for (email, container), user, role in zip(GAMERS, users, ROLES):
session_key = _make_session(user)
url = f"{base_url}/lyric/dev-login/{session_key}/?next={room_path}"
self.stdout.write(f"{container:<12} {email:<22} {role:<6} {url}")
self.stdout.write("")

View File

@@ -119,6 +119,9 @@ class DashboardMaintenanceTest(FunctionalTest):
class AppletMenuDismissTest(FunctionalTest): class AppletMenuDismissTest(FunctionalTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
# Portrait viewport: sidebars don't activate, h2 sits safely above
# #id_dash_content and can't be obscured by it regardless of font metrics.
self.browser.set_window_size(800, 1200)
Applet.objects.get_or_create(slug="username", defaults={"name": "Username"}) Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"}) Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
self.create_pre_authenticated_session("discoman@example.com") self.create_pre_authenticated_session("discoman@example.com")

View File

@@ -113,7 +113,7 @@ class NavbarByeTest(FunctionalTest):
class NavbarContGameTest(FunctionalTest): class NavbarContGameTest(FunctionalTest):
""" """
When the authenticated user has at least one room with a game event the When the authenticated user has at least one room with a game event the
CONT GAME btn-primary btn-xl appears in the navbar and navigates to that CONT GAME btn-primary appears in the navbar and navigates to that
room on confirmation. Its tooltip must also appear below the button. room on confirmation. Its tooltip must also appear below the button.
""" """
@@ -139,7 +139,6 @@ class NavbarContGameTest(FunctionalTest):
) )
btn = self.browser.find_element(By.ID, "id_cont_game") btn = self.browser.find_element(By.ID, "id_cont_game")
self.assertIn("btn-primary", btn.get_attribute("class")) self.assertIn("btn-primary", btn.get_attribute("class"))
self.assertIn("btn-xl", btn.get_attribute("class"))
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# T6 — CONT GAME tooltip appears below btn # # T6 — CONT GAME tooltip appears below btn #

View File

@@ -34,14 +34,15 @@ def _assign_all_roles(room, role_order=None):
slug="earthman", slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True}, defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
) )
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs) # Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"} _NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"): for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
for number in (11, 12, 13, 14): for number in (11, 12, 13, 14):
TarotCard.objects.get_or_create( TarotCard.objects.get_or_create(
deck_variant=earthman, deck_variant=earthman,
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em", slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
defaults={"arcana": "MINOR", "suit": suit, "number": number, defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
"name": f"{_NAME[number]} of {suit.capitalize()}"}, "name": f"{_NAME[number]} of {suit.capitalize()}"},
) )
for number, name, slug in [ for number, name, slug in [
@@ -138,3 +139,234 @@ class SigSelectChannelsTest(ChannelsFunctionalTest):
)) ))
return b return b
def _setup_sig_select_room(self):
"""Create a full SIG_SELECT room; return (room, [user_pc, user_nc, ...])."""
emails = [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
]
founder, _ = User.objects.get_or_create(email=emails[0])
room = Room.objects.create(name="Cursor Colour Test", owner=founder)
gamers = _fill_room_via_orm(room, emails)
_assign_all_roles(room)
return room, gamers
# ── SC1: NC hover → PC sees mid cursor active, coloured --priYl ────────── #
@tag('channels')
def test_nc_hover_activates_mid_cursor_in_pc_browser(self):
"""
When NC (levity mid) hovers a card, PC (levity left) must see the
--mid cursor become active, coloured --priYl (rgb 255 207 52).
Verifies: WS broadcast pipeline + JS applyHover + CSS role colouring.
"""
room, gamers = self._setup_sig_select_room()
room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"
# ── Browser 1: PC (founder) ───────────────────────────────────────────
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# ── Browser 2: NC (amigo) ─────────────────────────────────────────────
browser2 = self._make_browser2("amigo@test.io")
try:
browser2.get(room_url)
self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# Grab the first card ID visible in browser2's deck
first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
card_id = first_card.get_attribute("data-card-id")
# Hover over it — triggers sendHover() → WS broadcast
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(browser2).move_to_element(first_card).perform()
# ── Browser 1 should see --mid cursor go active (anchor carries class) ─
mid_cursor_sel = f'.sig-card[data-card-id="{card_id}"] .sig-cursor--mid'
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, mid_cursor_sel + ".active"
)
)
# CSS colour check: portal float has data-role="NC" → --priYl = 255, 207, 52
portal_sel = '.sig-cursor-float[data-role="NC"]'
portal_cursor = self.browser.find_element(By.CSS_SELECTOR, portal_sel)
color = self.browser.execute_script(
"return window.getComputedStyle(arguments[0]).color",
portal_cursor,
)
self.assertEqual(color, "rgb(255, 207, 52)", f"Expected --priYl colour for NC cursor, got {color}")
# ── Mouse-off: anchor class removed, portal float gone ────────────
ActionChains(browser2).move_to_element(
browser2.find_element(By.CSS_SELECTOR, ".sig-stage")
).perform()
self.wait_for(
lambda: not self.browser.find_elements(
By.CSS_SELECTOR, mid_cursor_sel + ".active"
)
)
finally:
browser2.quit()
# ── SC2: NC reserves → PC sees card border coloured --priYl ──────────── #
@tag('channels')
def test_nc_reservation_glows_priYl_in_pc_browser(self):
"""
When NC (levity mid) clicks OK on a card, PC must see that card's border
coloured --priYl (rgb 255 207 52) via the data-reserved-by CSS selector.
Verifies: sig_reserve view → WS broadcast → applyReservation → CSS glow.
"""
room, gamers = self._setup_sig_select_room()
room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"
# ── Browser 1: PC (founder) ───────────────────────────────────────────
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(room_url)
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# ── Browser 2: NC (amigo) ─────────────────────────────────────────────
browser2 = self._make_browser2("amigo@test.io")
try:
browser2.get(room_url)
self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# Get first card in B2's deck
first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
card_id = first_card.get_attribute("data-card-id")
# Click card body → .sig-focused → OK button appears
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(browser2).move_to_element(first_card).perform()
first_card.click()
ok_btn = self.wait_for(
lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-focused .sig-ok-btn")
)
ok_btn.click()
# ── B1 should see the card's border turn --priYl ──────────────────
reserved_card_sel = f'.sig-card[data-card-id="{card_id}"]'
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, reserved_card_sel + '[data-reserved-by="NC"]'
)
)
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
box_shadow = self.browser.execute_script(
"return window.getComputedStyle(arguments[0]).boxShadow",
reserved_card,
)
self.assertIn(
"255, 207, 52", box_shadow,
f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
)
finally:
browser2.quit()
# ── Polarity theming: qualifier text + no correspondence ─────────────────────
class SigSelectThemeTest(FunctionalTest):
"""Polarity-qualifier display (Graven/Leavened) and correspondence suppression.
No WebSocket needed — stage updates are local; uses plain FunctionalTest."""
EMAILS = [
"founder@test.io", "amigo@test.io", "bud@test.io",
"pal@test.io", "dude@test.io", "bro@test.io",
]
def setUp(self):
super().setUp()
self.browser.set_window_size(800, 1200)
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
def _setup_sig_room(self):
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
room = Room.objects.create(name="Theme Test", owner=founder)
_fill_room_via_orm(room, self.EMAILS)
_assign_all_roles(room)
return room
def _hover_card(self, css):
from selenium.webdriver.common.action_chains import ActionChains
card = self.browser.find_element(By.CSS_SELECTOR, css)
ActionChains(self.browser).move_to_element(card).perform()
return card
# ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── #
def test_levity_non_major_card_shows_leavened_above(self):
"""Hovering a non-major card in the levity overlay shows 'Leavened' in
qualifier-above and nothing in qualifier-below."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
)
self.assertEqual(above.text, "Leavened")
below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
self.assertEqual(below.text, "")
def test_levity_major_card_shows_leavened_below(self):
"""Hovering a major arcana card in the levity overlay shows 'Leavened' in
qualifier-below and nothing in qualifier-above."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io") # PC = levity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Major Arcana"]')
below = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
)
self.assertEqual(below.text, "Leavened")
above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
self.assertEqual(above.text, "")
# ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── #
def test_gravity_non_major_card_shows_graven_above(self):
"""EC (bud) sees the gravity overlay; hovering a non-major card shows 'Graven'."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("bud@test.io") # EC = gravity
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
above = self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
)
self.assertEqual(above.text, "Graven")
# ── ST3: Correspondence not shown ─────────────────────────────────────── #
def test_correspondence_not_shown_in_sig_select(self):
"""The Minchiate-equivalence field must always be blank on the stage card."""
room = self._setup_sig_room()
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
# Hover any card — correspondence should remain empty regardless
self._hover_card(".sig-card")
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sig-stage-card"
))
corr = self.browser.find_element(By.CSS_SELECTOR, ".fan-card-correspondence")
self.assertEqual(corr.text, "")

View File

@@ -1,12 +1,12 @@
describe("SigSelect", () => { describe("SigSelect", () => {
let testDiv, stageCard, card; let testDiv, stageCard, card, statBlock;
function makeFixture({ reservations = '{}' } = {}) { function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
testDiv = document.createElement("div"); testDiv = document.createElement("div");
testDiv.innerHTML = ` testDiv.innerHTML = `
<div class="sig-overlay" <div class="sig-overlay"
data-polarity="levity" data-polarity="${polarity}"
data-user-role="PC" data-user-role="${userRole}"
data-reserve-url="/epic/room/test/sig-reserve" data-reserve-url="/epic/room/test/sig-reserve"
data-reservations="${reservations.replace(/"/g, '&quot;')}"> data-reservations="${reservations.replace(/"/g, '&quot;')}">
<div class="sig-modal"> <div class="sig-modal">
@@ -15,10 +15,35 @@ describe("SigSelect", () => {
<span class="fan-corner-rank"></span> <span class="fan-corner-rank"></span>
<i class="stage-suit-icon"></i> <i class="stage-suit-icon"></i>
<p class="fan-card-name-group"></p> <p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<h3 class="fan-card-name"></h3> <h3 class="fan-card-name"></h3>
<p class="sig-qualifier-below"></p>
<p class="fan-card-arcana"></p> <p class="fan-card-arcana"></p>
<p class="fan-card-correspondence"></p> <p class="fan-card-correspondence"></p>
</div> </div>
<div class="sig-stat-block">
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Upright</p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversed</p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div>
<button class="btn btn-nav-left sig-caution-prev" type="button">&#9664;</button>
<button class="btn btn-nav-right sig-caution-next" type="button">&#9654;</button>
<div class="sig-caution-tooltip" id="id_sig_caution">
<div class="sig-caution-header">
<h4 class="sig-caution-title">Caution!</h4>
<span class="sig-caution-type">Rival Interaction</span>
</div>
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
<p class="sig-caution-effect"></p>
<span class="sig-caution-index"></span>
</div>
</div>
</div> </div>
<div class="sig-deck-grid"> <div class="sig-deck-grid">
<div class="sig-card" <div class="sig-card"
@@ -28,7 +53,10 @@ describe("SigSelect", () => {
data-name-group="Pentacles" data-name-group="Pentacles"
data-name-title="King of Pentacles" data-name-title="King of Pentacles"
data-arcana="Minor Arcana" data-arcana="Minor Arcana"
data-correspondence=""> data-correspondence=""
data-keywords-upright="action,impulsiveness,ambition"
data-keywords-reversed="no direction,disregard for consequences"
data-cautions="${cardCautions.replace(/"/g, '&quot;')}">
<div class="fan-card-corner fan-card-corner--tl"> <div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">K</span> <span class="fan-corner-rank">K</span>
</div> </div>
@@ -48,6 +76,7 @@ describe("SigSelect", () => {
`; `;
document.body.appendChild(testDiv); document.body.appendChild(testDiv);
stageCard = testDiv.querySelector(".sig-stage-card"); stageCard = testDiv.querySelector(".sig-stage-card");
statBlock = testDiv.querySelector(".sig-stat-block");
card = testDiv.querySelector(".sig-card"); card = testDiv.querySelector(".sig-card");
window.fetch = jasmine.createSpy("fetch").and.returnValue( window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true }) Promise.resolve({ ok: true })
@@ -107,80 +136,6 @@ describe("SigSelect", () => {
}); });
}); });
// ── Touch: OK btn tap allows synthetic click through ──────────────── //
describe("touch on OK button", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("touchstart on OK btn does not call preventDefault (allows synthetic click)", () => {
// First tap the card body to show OK
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(card.classList.contains("sig-focused")).toBe(true);
// Now tap the OK button — touchstart should NOT preventDefault
var okBtn = card.querySelector(".sig-ok-btn");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: okBtn })],
});
okBtn.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("touchstart on card body (not OK btn) calls preventDefault", () => {
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(true);
});
});
// ── Touch outside grid dismisses stage (mobile) ───────────────────── //
describe("touch outside grid", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("dismisses stage preview when touching outside the grid (unfocused state)", () => {
// Focus a card first
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(stageCard.style.display).toBe("");
// Touch on the sig-stage (outside the grid)
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 2, target: stage })],
}));
expect(stageCard.style.display).toBe("none");
expect(card.classList.contains("sig-focused")).toBe(false);
});
it("does NOT dismiss stage preview when frozen (card reserved)", () => {
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
SigSelect._setFrozen(true);
// _focusedCardEl is set but frozen — use internal state trick via _setFrozen
// We also need a focused card; simulate it by setting frozen after focus
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 3, target: stage })],
}));
expect(stageCard.style.display).toBe("");
});
});
// ── Lock after reservation ─────────────────────────────────────────── // // ── Lock after reservation ─────────────────────────────────────────── //
describe("lock after reservation", () => { describe("lock after reservation", () => {
@@ -200,18 +155,6 @@ describe("SigSelect", () => {
expect(window.fetch).not.toHaveBeenCalled(); expect(window.fetch).not.toHaveBeenCalled();
}); });
it("does not call preventDefault on touchstart while a card is reserved", () => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
SigSelect._setReservedCardId("99");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("allows focus again after reservation is cleared", () => { it("allows focus again after reservation is cleared", () => {
SigSelect._setReservedCardId("99"); SigSelect._setReservedCardId("99");
SigSelect._setReservedCardId(null); SigSelect._setReservedCardId(null);
@@ -252,4 +195,414 @@ describe("SigSelect", () => {
expect(card.classList.contains("sig-focused")).toBe(true); expect(card.classList.contains("sig-focused")).toBe(true);
}); });
}); });
// ── Caution tooltip (!!) ──────────────────────────────────────────── //
describe("caution tooltip", () => {
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
beforeEach(() => {
makeFixture();
cautionTooltip = testDiv.querySelector(".sig-caution-tooltip");
cautionEffect = testDiv.querySelector(".sig-caution-effect");
cautionPrev = testDiv.querySelector(".sig-caution-prev");
cautionNext = testDiv.querySelector(".sig-caution-next");
cautionBtn = testDiv.querySelector(".sig-caution-btn");
});
function hover() {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
}
function openCaution() {
hover();
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
it("!! click adds .sig-caution-open to the stage", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("FYI click when btn-disabled does not close caution", () => {
openCaution();
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("shows placeholder text when cautions list is empty", () => {
card.dataset.cautions = "[]";
openCaution();
expect(cautionEffect.innerHTML).toContain("pending");
});
it("renders first caution effect HTML including .card-ref spans", () => {
card.dataset.cautions = JSON.stringify(['First <span class="card-ref">Card</span> effect.']);
openCaution();
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
});
it("with 1 caution both nav arrows are disabled", () => {
card.dataset.cautions = JSON.stringify(["Single caution."]);
openCaution();
expect(cautionPrev.disabled).toBe(true);
expect(cautionNext.disabled).toBe(true);
});
it("with multiple cautions both nav arrows are always enabled", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]);
openCaution();
expect(cautionPrev.disabled).toBe(false);
expect(cautionNext.disabled).toBe(false);
});
it("next click advances to second caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Second");
});
it("next wraps from last caution back to first", () => {
card.dataset.cautions = JSON.stringify(["First", "Last"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev click goes back to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev wraps from first caution to last", () => {
card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]);
openCaution();
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Last");
});
it("index label shows n / total when multiple cautions", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
});
it("index label is empty when only 1 caution", () => {
card.dataset.cautions = JSON.stringify(["Only one."]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
});
it("card mouseleave closes the caution", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("opening again resets to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
// Close and reopen
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
openCaution();
expect(cautionEffect.innerHTML).toContain("First");
});
it("opening caution adds .btn-disabled and swaps labels to ×", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
expect(flipBtn.textContent).toBe("\u00D7");
expect(cautionBtn.textContent).toBe("\u00D7");
});
it("closing caution removes .btn-disabled and restores original labels", () => {
var flipBtn = testDiv.querySelector(".sig-flip-btn");
var origFlip = flipBtn.textContent;
var origCaution = cautionBtn.textContent;
openCaution();
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
expect(flipBtn.textContent).toBe(origFlip);
expect(cautionBtn.textContent).toBe(origCaution);
});
it("clicking the tooltip closes caution", () => {
openCaution();
cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("FLIP click when caution open (btn-disabled) does nothing", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
});
// ── Stat block: keyword population and FLIP toggle ────────────────── //
describe("stat block and FLIP", () => {
beforeEach(() => makeFixture());
it("populates upright keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_upright li");
expect(items.length).toBe(3);
expect(items[0].textContent).toBe("action");
expect(items[1].textContent).toBe("impulsiveness");
expect(items[2].textContent).toBe("ambition");
});
it("populates reversed keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("no direction");
expect(items[1].textContent).toBe("disregard for consequences");
});
it("FLIP click adds .is-reversed to the stat block", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(true);
});
it("second FLIP click removes .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("hovering a new card resets .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
new MouseEvent("click", { bubbles: true })
);
expect(statBlock.classList.contains("is-reversed")).toBe(true);
// Leave and re-enter (simulates moving to a different card)
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("card with no keywords yields empty lists", () => {
card.dataset.keywordsUpright = "";
card.dataset.keywordsReversed = "";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0);
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
});
});
// ── WS cursor hover (applyHover) ──────────────────────────────────────── //
//
// Fixture polarity = levity, userRole = PC.
// POLARITY_ROLES: levity → [PC, NC, SC] = [left, mid, right]
//
// Only tests the JS position mapping — colour is CSS-only.
describe("WS cursor hover", () => {
beforeEach(() => makeFixture());
it("NC hover activates the --mid cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
});
it("SC hover activates the --right cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "SC", active: true },
}));
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
});
it("own role (PC) hover event is ignored — no cursor activates", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "PC", active: true },
}));
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
});
it("hover-off removes .active from the cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: false },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
});
it("hover on unknown card_id is a no-op", () => {
expect(() => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 9999, role: "NC", active: true },
}));
}).not.toThrow();
});
});
// ── WS reservation — data-reserved-by attribute ───────────────────────── //
//
// applyReservation() sets data-reserved-by so the CSS can glow the card in
// the reserving gamer's role colour. These tests assert the attribute, not
// the colour (CSS variables aren't resolvable in the SpecRunner context).
describe("WS reservation sets data-reserved-by", () => {
beforeEach(() => makeFixture());
it("peer reservation sets data-reserved-by to the reserving role", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("NC");
});
it("peer reservation also adds .sig-reserved class", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.classList.contains("sig-reserved")).toBe(true);
});
it("release removes data-reserved-by", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(card.dataset.reservedBy).toBeUndefined();
});
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "PC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("PC");
expect(card.classList.contains("sig-reserved--own")).toBe(true);
});
it("peer reservation places a thumbs-up float and removes any hand-pointer float", () => {
// First, a hover float exists for NC (mid cursor)
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(document.querySelector('.sig-cursor-float[data-role="NC"]')).not.toBeNull();
expect(document.querySelector('.fa-hand-pointer[data-role="NC"]')).not.toBeNull();
// NC then clicks OK — reservation arrives
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
// Thumbs-up replaces hand-pointer
const floatEl = document.querySelector('.sig-cursor-float[data-role="NC"]');
expect(floatEl).not.toBeNull();
expect(floatEl.classList.contains("fa-thumbs-up")).toBe(true);
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
});
it("peer release removes the thumbs-up float", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
});
});
// ── Polarity theming — stage qualifier text ────────────────────────────── //
//
// On mouseenter, updateStage() injects "Leavened" or "Graven" into the
// sig-qualifier-above (non-major) or sig-qualifier-below (major arcana) slot.
// Correspondence field is never populated in sig-select context.
describe("polarity theming — stage qualifier", () => {
it("levity non-major card puts 'Leavened' in qualifier-above, qualifier-below empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// data-arcana defaults to "Minor Arcana" in fixture → non-major
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Leavened");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
});
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dataset.nameTitle = "The Schizo";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
});
it("non-major arcana title has no trailing comma", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// fixture default: Minor Arcana, "King of Pentacles"
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("King of Pentacles");
});
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
});
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
});
it("hovering clears qualifier slots from the previous card", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
// Now major — above should be empty, below filled
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("correspondence field is never populated", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.correspondence = "Il Bagatto (Minchiate)";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
});
});
}); });

View File

@@ -110,7 +110,8 @@
} }
// In landscape: shift gear btn and applet menus left of the footer right sidebar // In landscape: shift gear btn and applet menus left of the footer right sidebar
@media (orientation: landscape) and (max-width: 1440px) { // XL override below doubles sidebar to 8rem — centre items in the wider column.
@media (orientation: landscape) {
$sidebar-w: 4rem; $sidebar-w: 4rem;
.gameboard-page, .gameboard-page,
@@ -119,7 +120,7 @@
.room-page, .room-page,
.billboard-page { .billboard-page {
> .gear-btn { > .gear-btn {
right: 0.5rem; right: 1rem;
bottom: 3.95rem; // same gap above kit btn as portrait; no page-specific overrides needed bottom: 3.95rem; // same gap above kit btn as portrait; no page-specific overrides needed
top: auto; top: auto;
} }
@@ -131,12 +132,30 @@
#id_wallet_applet_menu, #id_wallet_applet_menu,
#id_room_menu, #id_room_menu,
#id_billboard_applet_menu { #id_billboard_applet_menu {
right: 0.5rem; right: 1rem;
bottom: 6.6rem; bottom: 6.6rem;
top: auto; top: auto;
} }
} }
@media (orientation: landscape) and (min-width: 1800px) {
// Centre gear btn and menus in the doubled 8rem sidebar (was 0.5rem from right edge)
.gameboard-page,
.dashboard-page,
.wallet-page,
.room-page,
.billboard-page {
> .gear-btn { right: 2.5rem; }
}
#id_dash_applet_menu,
#id_game_applet_menu,
#id_game_kit_menu,
#id_wallet_applet_menu,
#id_room_menu,
#id_billboard_applet_menu { right: 2.5rem; }
}
// ── Applet box visual shell (reusable outside the grid) ──── // ── Applet box visual shell (reusable outside the grid) ────
%applet-box { %applet-box {
border: border:
@@ -215,6 +234,16 @@
black 99%, black 99%,
transparent 100% transparent 100%
); );
margin-left: 1rem;
margin-top: 1rem;
@media (orientation: landscape) and (min-width: 900px) {
margin-left: 2rem;
margin-top: 2rem;
}
@media (orientation: landscape) and (min-width: 1800px) {
margin-left: 4rem;
margin-top: 4rem;
}
section { section {
@extend %applet-box; @extend %applet-box;

View File

@@ -34,6 +34,7 @@ body {
font-size: 2rem; font-size: 2rem;
} }
} }
.container-fluid { .container-fluid {
display: flex; display: flex;
@@ -192,8 +193,18 @@ body {
} }
} }
@media (orientation: landscape) and (max-width: 1440px) { @media (orientation: landscape) and (max-width: 1100px) {
$sidebar-w: 4rem; body .container {
.navbar {
h1 {
font-size: 1rem !important;
}
}
}
}
@media (orientation: landscape) {
$sidebar-w: 5rem;
// ── Sidebar layout: navbar ← left, footer → right ──────────────────────────── // ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
body { body {
@@ -264,13 +275,12 @@ body {
.navbar-label { opacity: 0.7; } .navbar-label { opacity: 0.7; }
} }
.btn-primary { // .btn-primary {
width: 3rem; // width: 4rem;
height: 3rem; // height: 4rem;
font-size: 0.75rem; // font-size: 0.875rem;
border-width: 0.125rem; // border-width: 0.21rem;
// margin-left: 0.75rem; // }
}
// Login form: offset from fixed sidebars in landscape // Login form: offset from fixed sidebars in landscape
.input-group { .input-group {
@@ -288,26 +298,35 @@ body {
} }
} }
// Container: fill center, compensate for fixed sidebars on both sides // Container: fill center, compensate for fixed sidebars on both sides.
// max-width: none overrides the @media (min-width: 1200px) rule above so the
// container fills all available space between the two sidebars on wide screens.
body .container { body .container {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
max-width: none;
margin-left: $sidebar-w; margin-left: $sidebar-w;
margin-right: $sidebar-w; margin-right: $sidebar-w;
padding: 0 0.5rem; padding: 0 0.5rem;
} }
// Header row: compact in landscape // Header row: h2 rotates into the left gutter (just right of the navbar border).
// position:fixed takes h2 out of flow; .row collapses to zero height automatically.
body .container .row { body .container .row {
padding: 0.25rem 0; padding: 0;
margin: 0;
.col-lg-6 h2 { }
font-size: 1.5rem; body .container .row .col-lg-6 h2 {
margin: 0 0 0.25rem; position: fixed;
letter-spacing: 0.4em; left: 5rem; // $sidebar-w — flush with the navbar right border
text-align: center; top: 50%;
text-align-last: left; transform: translateY(-50%) rotate(180deg);
} writing-mode: vertical-rl;
font-size: 1.5rem;
letter-spacing: 0.4em;
margin: 0;
z-index: 85;
pointer-events: none;
} }
// Footer → fixed right sidebar (mirrors navbar approach — explicit right boundary) // Footer → fixed right sidebar (mirrors navbar approach — explicit right boundary)
@@ -324,6 +343,7 @@ body {
align-items: center; align-items: center;
border-top: none; border-top: none;
border-left: 0.1rem solid rgba(var(--secUser), 0.3); border-left: 0.1rem solid rgba(var(--secUser), 0.3);
background-color: rgba(var(--priUser), 1); // opaque: masks tray sliding behind it
padding: 1rem 0; padding: 1rem 0;
gap: 0; gap: 0;
z-index: 100; z-index: 100;
@@ -332,7 +352,8 @@ body {
flex-direction: column-reverse; flex-direction: column-reverse;
width: auto; width: auto;
max-width: none; max-width: none;
gap: 3rem; gap: 1.5rem !important;
margin-bottom: 4rem;
a { a {
font-size: 1.75rem; font-size: 1.75rem;
@@ -344,13 +365,104 @@ body {
.footer-container { .footer-container {
position: absolute; position: absolute;
bottom: 0.75rem; top: 0.25rem;
text-align: center; text-align: center;
font-size: 0.55rem; line-height: 0.75 !important;
line-height: 1.4; color: rgba(var(--secUser), 1);
color: rgba(var(--secUser), 0.5);
br { display: block; } br { display: block; }
small {
font-size: 0.75rem !important;
}
}
}
}
@media (orientation: landscape) and (min-width: 700px) {
body .container .row .col-lg-6 h2 {
@media (min-height: 400px) {
font-size: 2.5rem;
}
@media (min-height: 500px) {
font-size: 3rem;
}
}
body #id_footer {
#id_footer_nav {
gap: 3rem !important;
a {
font-size: 1.75rem;
display: flex;
justify-content: center;
align-items: center;
}
}
.footer-container {
line-height: 1;
margin-top: 0.5rem;
small {
font-size: 1rem;
}
}
}
}
// ── XL landscape (≥1800px): double sidebar widths and scale content ────────────
@media (orientation: landscape) and (min-width: 1800px) {
$sidebar-xl: 8rem;
body .container .navbar {
width: $sidebar-xl;
.container-fluid {
gap: 2rem;
padding: 0 0.5rem;
}
.navbar-brand h1 { font-size: 2.4rem; }
.navbar-text { font-size: 0.78rem; } // 0.65rem × 1.2
// .btn-primary { width: 4rem; height: 4rem; font-size: 0.875rem; }
.input-group {
left: $sidebar-xl;
right: $sidebar-xl;
}
}
body .container {
margin-left: $sidebar-xl;
margin-right: $sidebar-xl;
}
// h2 page title: keep vertical rotation; shift left to clear the wider XL navbar.
body .container .row .col-lg-6 h2 {
left: 8rem; // $sidebar-xl
@media (min-height: 800px) {
font-size: 4.5rem;
}
}
body #id_footer {
width: $sidebar-xl;
#id_footer_nav {
gap: 8rem !important;
a { font-size: 3rem; }
}
.footer-container {
font-size: 0.85rem;
margin-top: 1rem;
small {
font-size: 1.2rem;
}
} }
} }
} }
@@ -363,13 +475,6 @@ body {
.navbar-brand h1 { .navbar-brand h1 {
font-size: 1.2rem; font-size: 1.2rem;
} }
.btn-primary {
width: 3rem;
height: 3rem;
font-size: 0.75rem;
border-width: 0.125rem;
}
} }
.row .col-lg-6 h2 { .row .col-lg-6 h2 {
@@ -436,8 +541,8 @@ body {
br { display: none; } br { display: none; }
small { small {
font-size: 0.7rem; font-size: 0.75rem;
opacity: 0.6; opacity: 1;
} }
} }
} }

View File

@@ -25,6 +25,10 @@
} }
&.btn-primary { &.btn-primary {
width: 4rem;
height: 4rem;
font-size: 0.875rem;
border-width: 0.21rem;
color: rgba(var(--quaUser), 1); color: rgba(var(--quaUser), 1);
border-color: rgba(var(--quaUser), 1); border-color: rgba(var(--quaUser), 1);
background-color: rgba(var(--quiUser), 1); background-color: rgba(var(--quiUser), 1);
@@ -34,37 +38,6 @@
0.25rem 0.25rem 0.25rem rgba(var(--quiUser), 0.12) 0.25rem 0.25rem 0.25rem rgba(var(--quiUser), 0.12)
; ;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--quaUser), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--quaUser), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--quaUser), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--quaUser), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--quiUser), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--quaUser), 0.12)
;
}
}
&.btn-xl {
width: 4rem;
height: 4rem;
font-size: 0.875rem;
border-width: 0.21rem;
&:hover { &:hover {
text-shadow: text-shadow:
0.2rem 0.2rem 0.2rem rgba(0, 0, 0, 0.25), 0.2rem 0.2rem 0.2rem rgba(0, 0, 0, 0.25),
@@ -72,7 +45,7 @@
; ;
box-shadow: box-shadow:
0.24rem 0.24rem 0.5rem rgba(0, 0, 0, 0.25), 0.24rem 0.24rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--quaUser), 22) 0 0 0.5rem rgba(var(--quaUser), 0.22)
; ;
} }
@@ -87,7 +60,14 @@
-0.2rem -0.2rem 0.24rem rgba(0, 0, 0, 0.25), -0.2rem -0.2rem 0.24rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--quaUser), 0.22) 0 0 0.5rem rgba(var(--quaUser), 0.22)
; ;
} }
@media (orientation: landscape) and (max-width: 1100px) {
width: 2.75rem !important;
height: 2.75rem !important;
font-size: 0.625rem !important;
border-width: 0.125rem !important;
}
} }
&.btn-abandon { &.btn-abandon {
@@ -300,13 +280,20 @@
} }
} }
@media (orientation: landscape) and (min-width: 1800px) {
width: 2.4rem; // 2rem × 1.2
height: 2.4rem;
font-size: 0.75rem; // 0.63rem × 1.2
}
&.btn-disabled { &.btn-disabled {
cursor: default !important; cursor: default !important;
pointer-events: none;
font-size: 1.2rem; font-size: 1.2rem;
padding-bottom: 0.1rem; padding-bottom: 0.1rem;
color: rgba(var(--secUser), 0.25); color: rgba(var(--secUser), 0.25) !important;
background-color: rgba(var(--priUser), 1); background-color: rgba(var(--priUser), 1) !important;
border-color: rgba(var(--secUser), 0.25); border-color: rgba(var(--secUser), 0.25) !important;
box-shadow: box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5), 0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
@@ -336,4 +323,144 @@
; ;
} }
} }
&.btn-nav-left {
color: rgba(var(--priFs), 1);
border-color: rgba(var(--priFs), 1);
background-color: rgba(var(--terFs), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terFs), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terFs), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priFs), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priFs), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priFs), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priFs), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terFs), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priFs), 0.12)
;
}
}
&.btn-nav-right {
color: rgba(var(--priLm), 1);
border-color: rgba(var(--priLm), 1);
background-color: rgba(var(--terLm), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terLm), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terLm), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priLm), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priLm), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priLm), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terLm), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
}
&.btn-reverse {
color: rgba(var(--priCy), 1);
border-color: rgba(var(--priCy), 1);
background-color: rgba(var(--terCy), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terCy), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terCy), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priCy), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priCy), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priCy), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priCy), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terCy), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priCy), 0.12)
;
}
}
&.btn-tip {
color: rgba(var(--priLm), 1);
border-color: rgba(var(--priLm), 1);
background-color: rgba(var(--terLm), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terLm), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terLm), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priLm), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priLm), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priLm), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terLm), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priLm), 0.12)
;
}
}
} }

View File

@@ -0,0 +1,672 @@
// ─── Card deck primitives — fan cards + sig-select overlay ─────────────────────
//
// Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav)
// extracted from _game-kit.scss; sig-select overlay extracted from _room.scss.
// ── Tarot fan modal ──────────────────────────────────────────────────────────
#id_tarot_fan_dialog {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border: none;
background: rgba(0, 0, 0, 0.88);
overflow: hidden;
&::backdrop { display: none; } // Dialog IS the backdrop
}
.tarot-fan-wrap {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
perspective: 900px;
button {
box-shadow: none;
&:hover, &.active {
box-shadow: none;
}
}
}
.tarot-fan {
position: relative;
width: 220px;
height: 340px;
}
.fan-card {
position: absolute;
inset: 0;
width: 220px;
height: 340px;
border-radius: 0.75rem;
background: rgba(var(--priUser), 1);
border: 0.1rem solid rgba(var(--secUser), 0.4);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.25s ease, opacity 0.25s ease;
transform-style: preserve-3d;
&--active {
border-color: rgba(var(--secUser), 1);
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
}
}
.fan-card-corner {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
line-height: 1;
color: rgba(var(--secUser), 0.75);
&--tl { top: 0.4rem; left: 0.4rem; }
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
.fan-corner-rank {
font-size: 1.5rem;
font-weight: bold;
padding: 0.18rem 0;
}
i { font-size: 1.5rem; }
}
.fan-card-face {
padding: 1.25rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.5rem;
.fan-card-number { font-size: 0.65rem; }
.fan-card-name-group { font-size: 0.65rem; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; color: rgba(var(--terUser), 1); }
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
.fan-card-correspondence { font-size: 0.6rem; font-style: italic; color: rgba(var(--secUser), 0.5); }
}
.fan-nav {
position: absolute;
z-index: 20;
font-size: 3rem;
line-height: 1;
background: none;
border: none;
color: rgba(var(--secUser), 0.6);
cursor: pointer;
padding: 1rem;
transition: color 0.15s;
pointer-events: auto;
&:hover { color: rgba(var(--secUser), 1); }
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
&:focus:not(:focus-visible) { outline: none; box-shadow: none; }
&--prev { left: 1rem; }
&--next { right: 1rem; }
}
// ─── Sig Select overlay (SIG_SELECT phase) ────────────────────────────────────
//
// Two overlays (levity / gravity) run in parallel, one per polarity group.
// Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal.
// Inside the modal: upper stage (card preview) + lower mini card grid (no scroll).
html:has(.sig-backdrop) {
overflow: hidden;
}
.sig-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(5px);
z-index: 100;
pointer-events: none;
}
.sig-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: stretch;
justify-content: center;
z-index: 120;
pointer-events: none;
}
.sig-modal {
pointer-events: auto;
display: flex;
flex-direction: column;
width: 100%; // respects overlay padding-right set by JS
max-width: 420px;
max-height: 100%; // respects overlay padding-bottom set by JS
}
// ─── Stage ────────────────────────────────────────────────────────────────────
// flex: 1 — fills all space above the card grid; no background (backdrop blur).
// Row layout: preview card bottom-left, stat block fills the right.
// Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or
// 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle
// container query units inside min().
.sig-stage {
flex: 1;
min-height: 0;
position: relative;
display: flex;
flex-direction: row;
align-items: flex-end;
padding-left: 1.5rem;
gap: 0.75rem;
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
.sig-stage-card {
flex-shrink: 0;
width: var(--sig-card-w, 120px);
height: auto;
aspect-ratio: 5 / 8;
border-radius: 0.5rem;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 0.6);
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
// so these just need display/font overrides; the corners land at the card edges.
// All font-sizes scale with --sig-card-w (ratio = original-rem × 16 / 120).
.fan-card-corner--tl {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.133); font-weight: 700; }
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
}
.fan-card-corner--br {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.12); font-weight: 700; }
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
}
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.25rem 0.15rem;
gap: 0.2rem;
.fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; }
.sig-qualifier-above,
.sig-qualifier-below { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
.fan-card-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
.fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
.fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only
}
}
// Stat block — same dimensions as the preview card (width × 5:8 aspect).
// flex: 0 0 auto so it doesn't stretch to fill the stage; the rest of the
// stage row is simply empty, giving the card room to breathe.
.sig-stat-block {
flex: 0 0 auto;
width: var(--sig-card-w, 120px);
height: calc(var(--sig-card-w, 120px) * 8 / 5);
align-self: flex-end;
background: rgba(var(--priUser), 0.5);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.15);
display: none;
position: relative;
.sig-flip-btn {
position: absolute;
top: -1rem;
right: -1rem;
margin: 0;
z-index: 50;
}
.sig-caution-btn {
position: absolute;
top: 1.25rem;
right: -1rem;
margin: 0;
z-index: 50;
}
// Caution tooltip — covers the entire stat block (inset: 0), z-index above buttons.
.sig-caution-tooltip {
display: none;
position: absolute;
inset: 0;
z-index: 60;
background-color: rgba(var(--tooltip-bg), 0.6);
backdrop-filter: blur(6px);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--priYl), 0.35);
padding: 0.75rem;
flex-direction: column;
gap: 0.4rem;
overflow-y: auto;
}
.sig-caution-header {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.sig-caution-title {
font-size: calc(var(--sig-card-w, 120px) * 0.093);
font-weight: 700;
margin: 0;
color: rgba(var(--priYl), 1);
}
.sig-caution-type {
font-size: calc(var(--sig-card-w, 120px) * 0.058);
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.05em;
flex-shrink: 0;
}
.sig-caution-shoptalk {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
opacity: 0.55;
margin: 0;
font-style: italic;
}
.sig-caution-effect {
flex: 1;
font-size: calc(var(--sig-card-w, 120px) * 0.075);
margin: 0;
line-height: 1.55;
.card-ref {
color: rgba(var(--terUser), 1);
font-weight: 600;
}
}
.sig-caution-index {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
opacity: 0.55;
}
// Nav arrows portaled out of tooltip — sit at bottom corners above tooltip (z-70)
.sig-caution-prev,
.sig-caution-next {
display: none;
position: absolute;
bottom: -1rem;
margin: 0;
z-index: 70;
}
.sig-caution-prev { left: -1rem; }
.sig-caution-next { right: -1rem; }
.stat-face {
display: none;
padding: calc(var(--sig-card-w, 120px) * 0.37) calc(var(--sig-card-w, 120px) * 0.1) calc(var(--sig-card-w, 120px) * 0.08);
&--upright { display: block; }
}
&.is-reversed {
.stat-face--upright { display: none; }
.stat-face--reversed { display: block; }
}
.stat-face-label {
font-size: calc(var(--sig-card-w, 120px) * 0.063);
text-transform: uppercase;
letter-spacing: 0.09em;
opacity: 0.4;
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
}
.stat-keywords {
list-style: none;
padding: 0;
margin: 0;
li {
font-size: calc(var(--sig-card-w, 120px) * 0.083);
padding: calc(var(--sig-card-w, 120px) * 0.042) 0;
opacity: 0.85;
border-bottom: 0.05rem solid rgba(var(--terUser), 0.12);
&:last-child { border-bottom: none; }
}
}
}
&.sig-stage--frozen .sig-stat-block { display: block; }
&.sig-caution-open .sig-stat-block {
.sig-caution-tooltip { display: flex; }
.sig-caution-prev, .sig-caution-next { display: inline-flex; }
}
}
// ─── Mini card grid ───────────────────────────────────────────────────────────
// flex: 0 0 auto — shrinks to card content; no background (backdrop blur).
// align-content: start prevents CSS grid from distributing extra height between rows.
.sig-deck-grid {
flex: 0 0 auto;
display: grid;
grid-template-columns: repeat(6, 1fr);
align-content: start;
gap: 2px;
padding: 4px;
overflow: hidden;
margin: 0 1rem 5rem 4rem;
}
.sig-card {
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
background: rgba(var(--priUser), 0.97);
border: 1px solid rgba(var(--secUser), 0.3);
position: relative;
cursor: grab;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem }
// Override: center the element within the card instead.
.fan-card-corner--tl {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
// OK / NVM overlay — appears on click (focused) or own reservation
.sig-card-actions {
position: absolute;
inset: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: rgba(var(--priUser), 0.92);
border-radius: inherit;
.sig-nvm-btn { display: none; }
}
&.sig-focused .sig-card-actions { display: flex; }
&.sig-reserved--own .sig-card-actions {
display: flex;
.sig-ok-btn { display: none; }
.sig-nvm-btn { display: flex; }
}
// Cursor strip — hangs below the card bottom edge; overflow: visible allows this.
.sig-card-cursors {
position: absolute;
bottom: -0.6rem;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 0 2px;
}
// Rise above DOM-order siblings when a peer's cursor is active on this card.
// Without this, later cards in the grid paint over the overflowing cursor icons.
&:has(.sig-cursor.active) { z-index: 5; }
&:hover:not([data-reserved-by]) {
border-color: rgba(var(--secUser), 0.8);
box-shadow: 0 0 4px rgba(var(--secUser), 0.25);
}
&.sig-reserved {
cursor: not-allowed;
}
// Role-coloured reservation glow — border/shadow matches the reserving gamer's role.
// data-reserved-by is set by applyReservation() in sig-select.js.
// Own reservation also shows role colour (same as peers see), not a separate style.
&.sig-reserved {
&[data-reserved-by="PC"] { border-color: rgba(var(--priRd), 1); box-shadow: 0 0 0 2px rgba(var(--priRd), 1); }
&[data-reserved-by="NC"] { border-color: rgba(var(--priYl), 1); box-shadow: 0 0 0 2px rgba(var(--priYl), 1); }
&[data-reserved-by="EC"] { border-color: rgba(var(--priGn), 1); box-shadow: 0 0 0 2px rgba(var(--priGn), 1); }
&[data-reserved-by="SC"] { border-color: rgba(var(--priCy), 1); box-shadow: 0 0 0 2px rgba(var(--priCy), 1); }
&[data-reserved-by="AC"] { border-color: rgba(var(--priId), 1); box-shadow: 0 0 0 2px rgba(var(--priId), 1); }
&[data-reserved-by="BC"] { border-color: rgba(var(--priFs), 1); box-shadow: 0 0 0 2px rgba(var(--priFs), 1); }
}
&.sig-reserved--own {
cursor: grabbing;
}
}
// ─── Cursor anchors ───────────────────────────────────────────────────────────
//
// Three tiny dots along the bottom of each mini card, one per role in the group.
// Inactive: invisible. Active (another gamer is hovering): role-coloured dot.
// Position order is fixed per polarity (POLARITY_ROLES in sig-select.js):
// levity (PC / NC / SC) → left / mid / right
// gravity (BC / EC / AC) → left / mid / right
// In-card cursor elements — invisible anchors only.
// Visible icons are portaled to document root by applyHover() in sig-select.js.
.sig-cursor {
display: block;
font-size: 0; // zero-size: no layout impact, just carries .active class
color: transparent;
pointer-events: none;
}
// ─── Floating cursor portal ───────────────────────────────────────────────────
//
// sig-select.js creates these <i> elements inside #id_sig_cursor_portal, a
// position:fixed root-level container, so they escape all overflow/clip contexts.
// Positioned via getBoundingClientRect() on the card element.
#id_sig_cursor_portal {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 200; // above sig-overlay (120), below tray (310)
overflow: visible;
}
.sig-cursor-float {
position: absolute;
font-size: 1.5rem;
line-height: 1;
transform: translateX(-50%); // centre on the x coordinate from JS
pointer-events: none;
}
// Role-specific colour + outline shadow + ninUser glow
.sig-cursor-float[data-role="PC"] {
color: rgba(var(--priRd), 1);
text-shadow: 2px 0 0 rgba(var(--priOr),1), -2px 0 0 rgba(var(--priOr),1),
0 2px 0 rgba(var(--priOr),1), 0 -2px 0 rgba(var(--priOr),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="NC"] {
color: rgba(var(--priYl), 1);
text-shadow: 2px 0 0 rgba(var(--priLm),1), -2px 0 0 rgba(var(--priLm),1),
0 2px 0 rgba(var(--priLm),1), 0 -2px 0 rgba(var(--priLm),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="EC"] {
color: rgba(var(--priGn), 1);
text-shadow: 2px 0 0 rgba(var(--priTk),1), -2px 0 0 rgba(var(--priTk),1),
0 2px 0 rgba(var(--priTk),1), 0 -2px 0 rgba(var(--priTk),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="SC"] {
color: rgba(var(--priCy), 1);
text-shadow: 2px 0 0 rgba(var(--priBl),1), -2px 0 0 rgba(var(--priBl),1),
0 2px 0 rgba(var(--priBl),1), 0 -2px 0 rgba(var(--priBl),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="AC"] {
color: rgba(var(--priId), 1);
text-shadow: 2px 0 0 rgba(var(--priVt),1), -2px 0 0 rgba(var(--priVt),1),
0 2px 0 rgba(var(--priVt),1), 0 -2px 0 rgba(var(--priVt),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
.sig-cursor-float[data-role="BC"] {
color: rgba(var(--priFs), 1);
text-shadow: 2px 0 0 rgba(var(--priMe),1), -2px 0 0 rgba(var(--priMe),1),
0 2px 0 rgba(var(--priMe),1), 0 -2px 0 rgba(var(--priMe),1),
0 0 6px rgba(0, 0, 0, 0.5);
}
// ─── Polarity theming — card colour inversion ────────────────────────────────
//
// Gravity (Graven): --priUser bg / --secUser text — standard dark palette.
// Levity (Leavened): --secUser bg / --priUser text — inverted, lighter feel.
// Both mini-cards and the stage preview card follow the same rule.
.sig-overlay[data-polarity="levity"] {
// Mini card: inverted palette. game-kit sets explicit colours on .fan-card-name
// and .fan-card-corner that out-specifc the parent color, so re-target them here.
.sig-card {
background: rgba(var(--secUser), 0.97);
border-color: rgba(var(--priUser), 0.3);
color: rgba(var(--priUser), 1);
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
.fan-card-name { color: rgba(var(--quiUser), 1); }
// OK / NVM overlay — must match the inverted card background
.sig-card-actions { background: rgba(var(--secUser), 0.92); }
}
// Stage preview card: same inversion + title colour.
// .fan-card-name-group and .fan-card-arcana have explicit color in the base
// .fan-card-face rule (specificity 0,2,0) — must re-target them here (0,3,0).
// Opacity dim is still applied by the nested sig-stage-card rule.
.sig-stage-card {
background: rgba(var(--secUser), 1);
border-color: rgba(var(--priUser), 0.6);
color: rgba(var(--priUser), 1);
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
.fan-card-name-group{ color: rgba(var(--priUser), 1); }
.fan-card-name { color: rgba(var(--quiUser), 1); }
.fan-card-arcana { color: rgba(var(--priUser), 1); }
}
// Polarity qualifier: same colour as the card title in this context
.sig-qualifier-above,
.sig-qualifier-below { color: rgba(var(--quiUser), 1); }
// card-ref spans inside the caution tooltip — must match the base rule's
// .sig-stat-block .sig-caution-effect .card-ref specificity (0,3,0) to win.
.sig-caution-effect .card-ref { color: rgba(var(--quiUser), 1); }
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
}
.sig-overlay[data-polarity="gravity"] {
// Stat block: invert priUser/secUser so gravity gets the same stark contrast as leavened cards
.sig-stat-block {
background: rgba(var(--secUser), 0.75);
color: rgba(var(--priUser), 1);
border-color: rgba(var(--priUser), 0.15);
}
// Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible —
// override to secUser (light) so body text reads against the dark backdrop.
.sig-caution-tooltip { color: rgba(var(--secUser), 1); }
// Polarity qualifier: terUser for gravity (quiUser is levity's equivalent)
.sig-qualifier-above,
.sig-qualifier-below { color: rgba(var(--terUser), 1); }
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
}
// ─── Sig select: landscape overrides ─────────────────────────────────────────
// Landscape base: 9×2 grid of 3rem cards. At ≥992px (wide enough for 18 cards
// at 3rem + 4rem left + ~4rem right): collapse to a single 18×1 row so the
// stage preview gets maximum vertical real-estate.
// padding-left clears the fixed left navbar (JS sets right/bottom but not left).
// Grid margins reset to 0 — overlay padding handles all edge clearance.
@media (orientation: landscape) {
.sig-modal {
max-width: none;
flex-direction: row; // grid to the right, stage + card preview to the left
margin-left: 4rem;
margin-right: 3rem;
}
.sig-stage {
min-width: 0; // allow shrinking in row layout; align-items:flex-end already set
}
.sig-deck-grid {
grid-template-columns: repeat(6, 2.5rem);
margin: 0;
align-self: flex-end; // sit at the bottom of the modal row
}
}
@media (orientation: landscape) and (min-width: 900px) {
// Wide landscape: revert to stacked layout (stage top, 18-card row grid bottom).
.sig-modal {
flex-direction: column;
align-items: stretch;
}
.sig-stage {
min-width: auto;
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem;
}
.sig-deck-grid {
grid-template-columns: repeat(18, 3rem);
align-self: center;
}
}
@media (orientation: landscape) and (min-width: 1800px) {
// Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem)
.sig-overlay { padding-left: 8rem; padding-right: 8rem; }
.sig-stage {
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem;
}
.sig-deck-grid {
grid-template-columns: repeat(18, 5rem);
align-self: center;
}
// Room menu: base right: 0.5rem (same-specificity ID rule) overrides _applets.scss
// XL block because _card-deck.scss is imported after _applets.scss. Re-declare here to win the cascade.
#id_room_menu { right: 2.5rem; }
}

View File

@@ -149,7 +149,7 @@ body.page-dashboard {
} }
} }
@media (orientation: landscape) and (max-width: 1440px) { @media (orientation: landscape) {
// Reset the 666px min-width so #id_dash_content shrinks to fit within the // Reset the 666px min-width so #id_dash_content shrinks to fit within the
// sidebar-bounded container rather than overflowing into the footer sidebar. // sidebar-bounded container rather than overflowing into the footer sidebar.
#id_dash_content { #id_dash_content {

View File

@@ -3,12 +3,16 @@
bottom: 0.5rem; bottom: 0.5rem;
right: 0.5rem; right: 0.5rem;
@media (orientation: landscape) and (max-width: 1440px) { @media (orientation: landscape) {
right: 0.5rem; right: 1rem;
bottom: 0.5rem; bottom: 0.5rem;
top: auto; top: auto;
} }
@media (orientation: landscape) and (min-width: 1800px) {
right: 2.5rem; // centre in doubled 8rem sidebar
}
z-index: 318; z-index: 318;
font-size: 1.75rem; font-size: 1.75rem;
cursor: pointer; cursor: pointer;
@@ -45,7 +49,7 @@
z-index: 316; z-index: 316;
overflow: hidden; overflow: hidden;
@media (orientation: landscape) and (max-width: 1440px) { @media (orientation: landscape) {
$sidebar-w: 4rem; $sidebar-w: 4rem;
// left: $sidebar-w; // left: $sidebar-w;
right: $sidebar-w; right: $sidebar-w;
@@ -208,118 +212,3 @@
opacity: 0.45; opacity: 0.45;
} }
// ── Tarot fan modal ──────────────────────────────────────────────────────────
#id_tarot_fan_dialog {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
border: none;
background: rgba(0, 0, 0, 0.88);
overflow: hidden;
&::backdrop { display: none; } // Dialog IS the backdrop
}
.tarot-fan-wrap {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
perspective: 900px;
button {
box-shadow: none;
&:hover, &.active {
box-shadow: none;
}
}
}
.tarot-fan {
position: relative;
width: 220px;
height: 340px;
}
.fan-card {
position: absolute;
inset: 0;
width: 220px;
height: 340px;
border-radius: 0.75rem;
background: rgba(var(--priUser), 1);
border: 0.1rem solid rgba(var(--secUser), 0.4);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.25s ease, opacity 0.25s ease;
transform-style: preserve-3d;
&--active {
border-color: rgba(var(--secUser), 1);
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
}
}
.fan-card-corner {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
line-height: 1;
color: rgba(var(--secUser), 0.75);
&--tl { top: 0.4rem; left: 0.4rem; }
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
.fan-corner-rank {
font-size: 1.5rem;
font-weight: bold;
padding: 0.18rem 0;
}
i { font-size: 1.5rem; }
}
.fan-card-face {
padding: 1.25rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.5rem;
.fan-card-number { font-size: 0.65rem; }
.fan-card-name-group { font-size: 0.65rem; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; color: rgba(var(--terUser), 1); }
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
.fan-card-correspondence { font-size: 0.6rem; font-style: italic; color: rgba(var(--secUser), 0.5); }
}
.fan-nav {
position: absolute;
z-index: 20;
font-size: 3rem;
line-height: 1;
background: none;
border: none;
color: rgba(var(--secUser), 0.6);
cursor: pointer;
padding: 1rem;
transition: color 0.15s;
pointer-events: auto;
&:hover { color: rgba(var(--secUser), 1); }
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
&:focus:not(:focus-visible) { outline: none; box-shadow: none; }
&--prev { left: 1rem; }
&--next { right: 1rem; }
}

View File

@@ -46,7 +46,7 @@ body.page-gameboard {
} }
} }
@media (orientation: landscape) and (max-width: 1440px) { @media (orientation: landscape) {
// Restore clip in landscape — overrides the >738px overflow:visible above, // Restore clip in landscape — overrides the >738px overflow:visible above,
// preventing the gameboard applets from bleeding into the footer sidebar. // preventing the gameboard applets from bleeding into the footer sidebar.
body.page-gameboard .container { body.page-gameboard .container {

View File

@@ -800,291 +800,43 @@ $card-h: 60px;
} }
// Landscape mobile — aggressively scale down to fit short viewport // Landscape mobile — aggressively scale down to fit short viewport
@media (orientation: landscape) and (max-width: 1440px) { @media (orientation: landscape) {
// Sink navbar below gate/role-select overlays when a modal is open. // Sink navbar + footer sidebar below any modal backdrop when open.
// Landscape navbar z-index is 100 (_base.scss); gate-backdrop/overlay are // Landscape navbar and footer sidebar are both z-index:100 (_base.scss).
// 100/120 — same level causes paint-order ties so we drop it to 50. // Gate/role-select/sig backdrops are also z-index:100 — DOM paint-order ties
// let the footer (later in DOM) bleed through. Drop both to 50.
html:has(.gate-backdrop) body .container .navbar, html:has(.gate-backdrop) body .container .navbar,
html:has(.role-select-backdrop) body .container .navbar { html:has(.role-select-backdrop) body .container .navbar,
html:has(.sig-backdrop) body .container .navbar {
z-index: 50;
}
html:has(.gate-backdrop) body #id_footer,
html:has(.role-select-backdrop) body #id_footer,
html:has(.sig-backdrop) body #id_footer {
z-index: 50; z-index: 50;
} }
// Reflow position strip into a vertical column along the left edge, // Position strip: horizontal row across the top, slots 1-6 in order.
// reversed so 6 is at top, 1 at bottom, below the GAMEROOM title. // Offset from both sidebars (5rem each) and centred with gap.
.position-strip { .position-strip {
flex-direction: column-reverse; flex-direction: row;
top: 3rem; top: 2.5rem;
left: 0.5rem; left: 5rem;
right: auto; right: 5rem;
justify-content: center;
gap: round($gate-gap * 0.4); gap: round($gate-gap * 0.4);
} }
// Shallow landscape (phones): wrap into two columns — left: 6,5,4 / right: 3,2,1 // Small landscape (phones ≤550px tall): strip stays horizontal — no two-column
// Columns grow rightward (wrap, not wrap-reverse) so overflow: hidden doesn't clip. // trick needed now that the h2 is in the gutter. Just clear any order overrides.
// order: -1 on slots 46 pulls them to the front of the flex sequence; combined
// with column-reverse they land in the left column reading 6,5,4 top-to-bottom.
@media (max-height: 550px) { @media (max-height: 550px) {
.position-strip { .position-strip {
flex-wrap: wrap; .gate-slot { order: 0; }
// cap height to exactly 3 circles so the 4th wraps to a new column top: 1rem;
max-height: #{3 * round($gate-node * 0.75) + 2 * round($gate-gap * 0.4)};
.gate-slot[data-slot="4"],
.gate-slot[data-slot="5"],
.gate-slot[data-slot="6"] { order: -1; }
} }
} }
} }
// ─── Sig Select overlay (SIG_SELECT phase) ────────────────────────────────────
//
// Two overlays (levity / gravity) run in parallel, one per polarity group.
// Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal.
// Inside the modal: upper stage (card preview) + lower mini card grid (no scroll).
html:has(.sig-backdrop) {
overflow: hidden;
}
.sig-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(5px);
z-index: 100;
pointer-events: none;
}
.sig-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: stretch;
justify-content: center;
z-index: 120;
pointer-events: none;
}
.sig-modal {
pointer-events: auto;
display: flex;
flex-direction: column;
width: 100%; // respects overlay padding-right set by JS
max-width: 420px;
max-height: 100%; // respects overlay padding-bottom set by JS
}
// ─── Stage ────────────────────────────────────────────────────────────────────
// flex: 1 — fills all space above the card grid; no background (backdrop blur).
// Row layout: preview card bottom-left, stat block fills the right.
// Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or
// 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle
// container query units inside min().
.sig-stage {
flex: 1;
min-height: 0;
display: flex;
flex-direction: row;
align-items: flex-end;
padding-left: 1.5rem;
gap: 0.75rem;
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
.sig-stage-card {
flex-shrink: 0;
width: var(--sig-card-w, 120px);
height: auto;
aspect-ratio: 5 / 8;
border-radius: 0.5rem;
background: rgba(var(--priUser), 1);
border: 0.15rem solid rgba(var(--secUser), 0.6);
display: flex;
flex-direction: column;
position: relative;
padding: 0.25rem;
overflow: hidden;
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
// so these just need display/font overrides; the corners land at the card edges.
.fan-card-corner--tl {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
.fan-card-corner--br {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.1;
gap: 0.1rem;
.fan-corner-rank { font-size: 0.9rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
.fan-card-face {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0.25rem 0.15rem;
gap: 0.2rem;
.fan-card-name-group { font-size: 0.55rem; opacity: 0.6; }
.fan-card-name { font-size: 0.7rem; font-weight: 600; }
.fan-card-arcana { font-size: 0.5rem; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
.fan-card-correspondence{ font-size: 0.5rem; opacity: 0.5; }
}
}
// Stat block — same height as the preview card; fills remaining stage width.
// Height derived from the JS-computed --sig-card-w via aspect ratio 5:8.
.sig-stat-block {
flex: 1;
height: calc(var(--sig-card-w, 120px) * 8 / 5);
align-self: flex-end;
background: rgba(var(--priUser), 0.25);
border-radius: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.15);
display: none;
}
&.sig-stage--frozen .sig-stat-block { display: block; }
}
// ─── Mini card grid ───────────────────────────────────────────────────────────
// flex: 0 0 auto — shrinks to card content; no background (backdrop blur).
// align-content: start prevents CSS grid from distributing extra height between rows.
.sig-deck-grid {
flex: 0 0 auto;
display: grid;
grid-template-columns: repeat(6, 1fr);
align-content: start;
gap: 2px;
padding: 4px;
overflow: hidden;
margin: 0 1rem 5rem 4rem;
}
.sig-card {
aspect-ratio: 5 / 8;
border-radius: 0.4rem;
background: rgba(var(--priUser), 0.97);
border: 1px solid rgba(var(--secUser), 0.3);
position: relative;
cursor: grab;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem }
// Override: center the element within the card instead.
.fan-card-corner--tl {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
i { font-size: 0.75rem; }
}
// OK / NVM overlay — appears on click (focused) or own reservation
.sig-card-actions {
position: absolute;
inset: 0;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
background: rgba(var(--priUser), 0.92);
border-radius: inherit;
.sig-nvm-btn { display: none; }
}
&.sig-focused .sig-card-actions { display: flex; }
&.sig-reserved--own .sig-card-actions {
display: flex;
.sig-ok-btn { display: none; }
.sig-nvm-btn { display: flex; }
}
// Cursor anchors strip — bottom of card
.sig-card-cursors {
position: absolute;
bottom: 2px;
left: 2px;
right: 2px;
display: flex;
justify-content: space-between;
}
&:hover:not([data-reserved-by]) {
border-color: rgba(var(--secUser), 0.8);
box-shadow: 0 0 4px rgba(var(--secUser), 0.25);
}
&.sig-reserved {
border-color: rgba(var(--terUser), 1);
box-shadow:
0 0 0.4rem rgba(var(--terUser), 0.7),
0 0 1rem rgba(var(--ninUser), 0.4);
cursor: not-allowed;
}
&.sig-reserved--own {
border-color: rgba(var(--secUser), 1);
box-shadow:
0 0 0.4rem rgba(var(--secUser), 0.7),
0 0 1rem rgba(var(--ninUser), 0.5);
cursor: grabbing;
}
}
// ─── Cursor anchors ───────────────────────────────────────────────────────────
//
// Three tiny dots along the bottom of each mini card, one per role in the group.
// Inactive: invisible. Active (another gamer is hovering): coloured dot.
.sig-cursor {
display: block;
width: 5px;
height: 5px;
border-radius: 50%;
background: transparent;
transition: background 0.1s;
&.active {
background: rgba(var(--terUser), 1);
box-shadow: 0 0 3px rgba(var(--ninUser), 0.8);
}
}
// ─── Sig select: landscape overrides ─────────────────────────────────────────
// Wider viewport → 2 rows of 9 cards; modal fills full available width.
// padding-left clears the fixed left navbar (JS sets right/bottom but not left).
// Grid margins reset to 0 — overlay padding handles all edge clearance in landscape.
@media (orientation: landscape) {
.sig-overlay { padding-left: 4rem; }
.sig-modal { max-width: none; }
.sig-deck-grid {
grid-template-columns: repeat(9, minmax(0, 90px));
margin: 0;
}
}
// ─── Seat tray — see _tray.scss ───────────────────────────────────────────── // ─── Seat tray — see _tray.scss ─────────────────────────────────────────────

View File

@@ -121,24 +121,29 @@ $handle-r: 1rem;
&::before { border-color: rgba(var(--quaUser), 1); } &::before { border-color: rgba(var(--quaUser), 1); }
} }
// ─── Role card: arc-in animation (portrait) ───────────────────────────────── // ─── Role card: scrawl fade-in ───────────────────────────────────────────────
@keyframes tray-role-arc-in { @keyframes tray-role-arc-in {
from { opacity: 0; transform: scale(0.3) translate(-40%, -40%); } from { opacity: 0; }
to { opacity: 1; transform: scale(1) translate(0, 0); } to { opacity: 1; }
} }
.tray-role-card { .tray-role-card {
background: rgba(var(--quaUser), 0.25); padding: 0;
display: flex; overflow: hidden;
align-items: flex-start; background: transparent;
justify-content: flex-start;
padding: 0.2em;
font-size: 0.65rem;
color: rgba(var(--quaUser), 1);
font-weight: 600;
&.arc-in { img {
animation: tray-role-arc-in 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards; display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transform: scale(1.4); // crop SVG's internal margins
}
// Cell stays static; only the scrawl image fades in.
&.arc-in img {
animation: tray-role-arc-in 1s ease forwards;
} }
} }
@@ -301,15 +306,7 @@ $handle-r: 1rem;
border-bottom: none; border-bottom: none;
} }
// Role card arc-in for landscape // Role card: same fade-in in landscape — no override needed.
@keyframes tray-role-arc-in-landscape {
from { opacity: 0; transform: scale(0.3) translate(-40%, 40%); }
to { opacity: 1; transform: scale(1) translate(0, 0); }
}
.tray-role-card.arc-in {
animation: tray-role-arc-in-landscape 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes tray-wobble-landscape { @keyframes tray-wobble-landscape {
0%, 100% { transform: translateY(0); } 0%, 100% { transform: translateY(0); }
@@ -328,3 +325,5 @@ $handle-r: 1rem;
80% { transform: translateY(-3px); } 80% { transform: translateY(-3px); }
} }
} }
// ≥1800px uses the same landscape tray rules as narrower landscape — no override block needed.

View File

@@ -6,6 +6,7 @@
@import 'gameboard'; @import 'gameboard';
@import 'palette-picker'; @import 'palette-picker';
@import 'room'; @import 'room';
@import 'card-deck';
@import 'tray'; @import 'tray';
@import 'billboard'; @import 'billboard';
@import 'game-kit'; @import 'game-kit';

View File

@@ -1,12 +1,12 @@
describe("SigSelect", () => { describe("SigSelect", () => {
let testDiv, stageCard, card; let testDiv, stageCard, card, statBlock;
function makeFixture({ reservations = '{}' } = {}) { function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
testDiv = document.createElement("div"); testDiv = document.createElement("div");
testDiv.innerHTML = ` testDiv.innerHTML = `
<div class="sig-overlay" <div class="sig-overlay"
data-polarity="levity" data-polarity="${polarity}"
data-user-role="PC" data-user-role="${userRole}"
data-reserve-url="/epic/room/test/sig-reserve" data-reserve-url="/epic/room/test/sig-reserve"
data-reservations="${reservations.replace(/"/g, '&quot;')}"> data-reservations="${reservations.replace(/"/g, '&quot;')}">
<div class="sig-modal"> <div class="sig-modal">
@@ -15,10 +15,35 @@ describe("SigSelect", () => {
<span class="fan-corner-rank"></span> <span class="fan-corner-rank"></span>
<i class="stage-suit-icon"></i> <i class="stage-suit-icon"></i>
<p class="fan-card-name-group"></p> <p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<h3 class="fan-card-name"></h3> <h3 class="fan-card-name"></h3>
<p class="sig-qualifier-below"></p>
<p class="fan-card-arcana"></p> <p class="fan-card-arcana"></p>
<p class="fan-card-correspondence"></p> <p class="fan-card-correspondence"></p>
</div> </div>
<div class="sig-stat-block">
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Upright</p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversed</p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div>
<button class="btn btn-nav-left sig-caution-prev" type="button">&#9664;</button>
<button class="btn btn-nav-right sig-caution-next" type="button">&#9654;</button>
<div class="sig-caution-tooltip" id="id_sig_caution">
<div class="sig-caution-header">
<h4 class="sig-caution-title">Caution!</h4>
<span class="sig-caution-type">Rival Interaction</span>
</div>
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
<p class="sig-caution-effect"></p>
<span class="sig-caution-index"></span>
</div>
</div>
</div> </div>
<div class="sig-deck-grid"> <div class="sig-deck-grid">
<div class="sig-card" <div class="sig-card"
@@ -28,7 +53,10 @@ describe("SigSelect", () => {
data-name-group="Pentacles" data-name-group="Pentacles"
data-name-title="King of Pentacles" data-name-title="King of Pentacles"
data-arcana="Minor Arcana" data-arcana="Minor Arcana"
data-correspondence=""> data-correspondence=""
data-keywords-upright="action,impulsiveness,ambition"
data-keywords-reversed="no direction,disregard for consequences"
data-cautions="${cardCautions.replace(/"/g, '&quot;')}">
<div class="fan-card-corner fan-card-corner--tl"> <div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">K</span> <span class="fan-corner-rank">K</span>
</div> </div>
@@ -48,6 +76,7 @@ describe("SigSelect", () => {
`; `;
document.body.appendChild(testDiv); document.body.appendChild(testDiv);
stageCard = testDiv.querySelector(".sig-stage-card"); stageCard = testDiv.querySelector(".sig-stage-card");
statBlock = testDiv.querySelector(".sig-stat-block");
card = testDiv.querySelector(".sig-card"); card = testDiv.querySelector(".sig-card");
window.fetch = jasmine.createSpy("fetch").and.returnValue( window.fetch = jasmine.createSpy("fetch").and.returnValue(
Promise.resolve({ ok: true }) Promise.resolve({ ok: true })
@@ -107,80 +136,6 @@ describe("SigSelect", () => {
}); });
}); });
// ── Touch: OK btn tap allows synthetic click through ──────────────── //
describe("touch on OK button", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("touchstart on OK btn does not call preventDefault (allows synthetic click)", () => {
// First tap the card body to show OK
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(card.classList.contains("sig-focused")).toBe(true);
// Now tap the OK button — touchstart should NOT preventDefault
var okBtn = card.querySelector(".sig-ok-btn");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: okBtn })],
});
okBtn.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("touchstart on card body (not OK btn) calls preventDefault", () => {
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(true);
});
});
// ── Touch outside grid dismisses stage (mobile) ───────────────────── //
describe("touch outside grid", () => {
beforeEach(() => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
makeFixture();
});
it("dismisses stage preview when touching outside the grid (unfocused state)", () => {
// Focus a card first
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(stageCard.style.display).toBe("");
// Touch on the sig-stage (outside the grid)
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 2, target: stage })],
}));
expect(stageCard.style.display).toBe("none");
expect(card.classList.contains("sig-focused")).toBe(false);
});
it("does NOT dismiss stage preview when frozen (card reserved)", () => {
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
SigSelect._setFrozen(true);
// _focusedCardEl is set but frozen — use internal state trick via _setFrozen
// We also need a focused card; simulate it by setting frozen after focus
var stage = testDiv.querySelector(".sig-stage");
stage.dispatchEvent(new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 3, target: stage })],
}));
expect(stageCard.style.display).toBe("");
});
});
// ── Lock after reservation ─────────────────────────────────────────── // // ── Lock after reservation ─────────────────────────────────────────── //
describe("lock after reservation", () => { describe("lock after reservation", () => {
@@ -200,18 +155,6 @@ describe("SigSelect", () => {
expect(window.fetch).not.toHaveBeenCalled(); expect(window.fetch).not.toHaveBeenCalled();
}); });
it("does not call preventDefault on touchstart while a card is reserved", () => {
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
SigSelect._setReservedCardId("99");
var touchEvent = new TouchEvent("touchstart", {
bubbles: true,
cancelable: true,
touches: [new Touch({ identifier: 1, target: card })],
});
card.dispatchEvent(touchEvent);
expect(touchEvent.defaultPrevented).toBe(false);
});
it("allows focus again after reservation is cleared", () => { it("allows focus again after reservation is cleared", () => {
SigSelect._setReservedCardId("99"); SigSelect._setReservedCardId("99");
SigSelect._setReservedCardId(null); SigSelect._setReservedCardId(null);
@@ -252,4 +195,414 @@ describe("SigSelect", () => {
expect(card.classList.contains("sig-focused")).toBe(true); expect(card.classList.contains("sig-focused")).toBe(true);
}); });
}); });
// ── Caution tooltip (!!) ──────────────────────────────────────────── //
describe("caution tooltip", () => {
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
beforeEach(() => {
makeFixture();
cautionTooltip = testDiv.querySelector(".sig-caution-tooltip");
cautionEffect = testDiv.querySelector(".sig-caution-effect");
cautionPrev = testDiv.querySelector(".sig-caution-prev");
cautionNext = testDiv.querySelector(".sig-caution-next");
cautionBtn = testDiv.querySelector(".sig-caution-btn");
});
function hover() {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
}
function openCaution() {
hover();
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
it("!! click adds .sig-caution-open to the stage", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("FYI click when btn-disabled does not close caution", () => {
openCaution();
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
});
it("shows placeholder text when cautions list is empty", () => {
card.dataset.cautions = "[]";
openCaution();
expect(cautionEffect.innerHTML).toContain("pending");
});
it("renders first caution effect HTML including .card-ref spans", () => {
card.dataset.cautions = JSON.stringify(['First <span class="card-ref">Card</span> effect.']);
openCaution();
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
});
it("with 1 caution both nav arrows are disabled", () => {
card.dataset.cautions = JSON.stringify(["Single caution."]);
openCaution();
expect(cautionPrev.disabled).toBe(true);
expect(cautionNext.disabled).toBe(true);
});
it("with multiple cautions both nav arrows are always enabled", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]);
openCaution();
expect(cautionPrev.disabled).toBe(false);
expect(cautionNext.disabled).toBe(false);
});
it("next click advances to second caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Second");
});
it("next wraps from last caution back to first", () => {
card.dataset.cautions = JSON.stringify(["First", "Last"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev click goes back to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("First");
});
it("prev wraps from first caution to last", () => {
card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]);
openCaution();
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(cautionEffect.innerHTML).toContain("Last");
});
it("index label shows n / total when multiple cautions", () => {
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
});
it("index label is empty when only 1 caution", () => {
card.dataset.cautions = JSON.stringify(["Only one."]);
openCaution();
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
});
it("card mouseleave closes the caution", () => {
openCaution();
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("opening again resets to first caution", () => {
card.dataset.cautions = JSON.stringify(["First", "Second"]);
openCaution();
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
// Close and reopen
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
openCaution();
expect(cautionEffect.innerHTML).toContain("First");
});
it("opening caution adds .btn-disabled and swaps labels to ×", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
expect(flipBtn.textContent).toBe("\u00D7");
expect(cautionBtn.textContent).toBe("\u00D7");
});
it("closing caution removes .btn-disabled and restores original labels", () => {
var flipBtn = testDiv.querySelector(".sig-flip-btn");
var origFlip = flipBtn.textContent;
var origCaution = cautionBtn.textContent;
openCaution();
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
expect(flipBtn.textContent).toBe(origFlip);
expect(cautionBtn.textContent).toBe(origCaution);
});
it("clicking the tooltip closes caution", () => {
openCaution();
cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
});
it("FLIP click when caution open (btn-disabled) does nothing", () => {
openCaution();
var flipBtn = testDiv.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
});
// ── Stat block: keyword population and FLIP toggle ────────────────── //
describe("stat block and FLIP", () => {
beforeEach(() => makeFixture());
it("populates upright keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_upright li");
expect(items.length).toBe(3);
expect(items[0].textContent).toBe("action");
expect(items[1].textContent).toBe("impulsiveness");
expect(items[2].textContent).toBe("ambition");
});
it("populates reversed keywords when a card is hovered", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("no direction");
expect(items[1].textContent).toBe("disregard for consequences");
});
it("FLIP click adds .is-reversed to the stat block", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(true);
});
it("second FLIP click removes .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
var flipBtn = statBlock.querySelector(".sig-flip-btn");
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("hovering a new card resets .is-reversed", () => {
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
new MouseEvent("click", { bubbles: true })
);
expect(statBlock.classList.contains("is-reversed")).toBe(true);
// Leave and re-enter (simulates moving to a different card)
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.classList.contains("is-reversed")).toBe(false);
});
it("card with no keywords yields empty lists", () => {
card.dataset.keywordsUpright = "";
card.dataset.keywordsReversed = "";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0);
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
});
});
// ── WS cursor hover (applyHover) ──────────────────────────────────────── //
//
// Fixture polarity = levity, userRole = PC.
// POLARITY_ROLES: levity → [PC, NC, SC] = [left, mid, right]
//
// Only tests the JS position mapping — colour is CSS-only.
describe("WS cursor hover", () => {
beforeEach(() => makeFixture());
it("NC hover activates the --mid cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
});
it("SC hover activates the --right cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "SC", active: true },
}));
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
});
it("own role (PC) hover event is ignored — no cursor activates", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "PC", active: true },
}));
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
});
it("hover-off removes .active from the cursor", () => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: false },
}));
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
});
it("hover on unknown card_id is a no-op", () => {
expect(() => {
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 9999, role: "NC", active: true },
}));
}).not.toThrow();
});
});
// ── WS reservation — data-reserved-by attribute ───────────────────────── //
//
// applyReservation() sets data-reserved-by so the CSS can glow the card in
// the reserving gamer's role colour. These tests assert the attribute, not
// the colour (CSS variables aren't resolvable in the SpecRunner context).
describe("WS reservation sets data-reserved-by", () => {
beforeEach(() => makeFixture());
it("peer reservation sets data-reserved-by to the reserving role", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("NC");
});
it("peer reservation also adds .sig-reserved class", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(card.classList.contains("sig-reserved")).toBe(true);
});
it("release removes data-reserved-by", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(card.dataset.reservedBy).toBeUndefined();
});
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "PC", reserved: true },
}));
expect(card.dataset.reservedBy).toBe("PC");
expect(card.classList.contains("sig-reserved--own")).toBe(true);
});
it("peer reservation places a thumbs-up float and removes any hand-pointer float", () => {
// First, a hover float exists for NC (mid cursor)
window.dispatchEvent(new CustomEvent("room:sig_hover", {
detail: { card_id: 42, role: "NC", active: true },
}));
expect(document.querySelector('.sig-cursor-float[data-role="NC"]')).not.toBeNull();
expect(document.querySelector('.fa-hand-pointer[data-role="NC"]')).not.toBeNull();
// NC then clicks OK — reservation arrives
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
// Thumbs-up replaces hand-pointer
const floatEl = document.querySelector('.sig-cursor-float[data-role="NC"]');
expect(floatEl).not.toBeNull();
expect(floatEl.classList.contains("fa-thumbs-up")).toBe(true);
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
});
it("peer release removes the thumbs-up float", () => {
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: true },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
detail: { card_id: 42, role: "NC", reserved: false },
}));
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
});
});
// ── Polarity theming — stage qualifier text ────────────────────────────── //
//
// On mouseenter, updateStage() injects "Leavened" or "Graven" into the
// sig-qualifier-above (non-major) or sig-qualifier-below (major arcana) slot.
// Correspondence field is never populated in sig-select context.
describe("polarity theming — stage qualifier", () => {
it("levity non-major card puts 'Leavened' in qualifier-above, qualifier-below empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// data-arcana defaults to "Minor Arcana" in fixture → non-major
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Leavened");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
});
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.arcana = "Major Arcana";
card.dataset.nameTitle = "The Schizo";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
});
it("non-major arcana title has no trailing comma", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
// fixture default: Minor Arcana, "King of Pentacles"
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("King of Pentacles");
});
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
});
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
makeFixture({ polarity: 'gravity', userRole: 'BC' });
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
});
it("hovering clears qualifier slots from the previous card", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
card.dataset.arcana = "Major Arcana";
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
// Now major — above should be empty, below filled
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
});
it("correspondence field is never populated", () => {
makeFixture({ polarity: 'levity', userRole: 'PC' });
card.dataset.correspondence = "Il Bagatto (Minchiate)";
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
});
});
}); });

View File

@@ -41,7 +41,7 @@
</div> </div>
{% endif %} {% endif %}
<button type="submit" class="btn btn-primary btn-xl">Share</button> <button type="submit" class="btn btn-primary">Share</button>
</form> </form>
<small>Note shared with: <small>Note shared with:
{% for user in note.shared_with.all %} {% for user in note.shared_with.all %}

View File

@@ -53,7 +53,7 @@
{% if room.gate_status == 'OPEN' %} {% if room.gate_status == 'OPEN' %}
<form method="POST" action="{% url 'epic:pick_roles' room.id %}" style="display:contents"> <form method="POST" action="{% url 'epic:pick_roles' room.id %}" style="display:contents">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="launch-game-btn btn btn-primary btn-xl">PICK ROLES</button> <button type="submit" class="launch-game-btn btn btn-primary">PICK ROLES</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>

View File

@@ -21,16 +21,40 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
</div> </div>
<div class="fan-card-face"> <div class="fan-card-face">
<p class="fan-card-name-group"></p> <p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<h3 class="fan-card-name"></h3> <h3 class="fan-card-name"></h3>
<p class="sig-qualifier-below"></p>
<p class="fan-card-arcana"></p> <p class="fan-card-arcana"></p>
<p class="fan-card-correspondence"></p> <p class="fan-card-correspondence"></p>{# not shown in sig-select — game-kit only #}
</div> </div>
<div class="fan-card-corner fan-card-corner--br"> <div class="fan-card-corner fan-card-corner--br">
<span class="fan-corner-rank"></span> <span class="fan-corner-rank"></span>
<i class="fa-solid stage-suit-icon" style="display:none"></i> <i class="fa-solid stage-suit-icon" style="display:none"></i>
</div> </div>
</div> </div>
<div class="sig-stat-block"></div> <div class="sig-stat-block">
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
<button class="btn btn-caution sig-caution-btn" type="button">FYI</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Upright</p>
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversed</p>
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
</div>
<div class="sig-caution-tooltip" id="id_sig_caution">
<div class="sig-caution-header">
<h4 class="sig-caution-title">Caution!</h4>
<p class="sig-caution-type">Rival Interaction</p>
</div>
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
<p class="sig-caution-effect"></p>
<span class="sig-caution-index"></span>
</div>
<button class="btn btn-nav-left sig-caution-prev" type="button">PRV</button>
<button class="btn btn-nav-right sig-caution-next" type="button">NXT</button>
</div>
</div> </div>
<div class="sig-deck-grid" id="id_sig_deck"> <div class="sig-deck-grid" id="id_sig_deck">
@@ -42,7 +66,10 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
data-name-group="{{ card.name_group }}" data-name-group="{{ card.name_group }}"
data-name-title="{{ card.name_title }}" data-name-title="{{ card.name_title }}"
data-arcana="{{ card.get_arcana_display }}" data-arcana="{{ card.get_arcana_display }}"
data-correspondence="{{ card.correspondence|default:'' }}"> data-correspondence="{{ card.correspondence|default:'' }}"
data-keywords-upright="{{ card.keywords_upright|join:',' }}"
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"
data-cautions="{{ card.cautions_json }}">
<div class="fan-card-corner fan-card-corner--tl"> <div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">{{ card.corner_rank }}</span> <span class="fan-corner-rank">{{ card.corner_rank }}</span>
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %} {% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
@@ -52,9 +79,9 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
<button class="sig-nvm-btn btn btn-cancel" type="button">NVM</button> <button class="sig-nvm-btn btn btn-cancel" type="button">NVM</button>
</div> </div>
<div class="sig-card-cursors"> <div class="sig-card-cursors">
<span class="sig-cursor sig-cursor--left"></span> <i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--left"></i>
<span class="sig-cursor sig-cursor--mid"></span> <i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--mid"></i>
<span class="sig-cursor sig-cursor--right"></span> <i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--right"></i>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -8,7 +8,7 @@
<form method="POST" action="{% url 'epic:confirm_token' room.id %}"> <form method="POST" action="{% url 'epic:confirm_token' room.id %}">
{% csrf_token %} {% csrf_token %}
{% if is_last_slot %} {% if is_last_slot %}
<button type="submit" class="btn btn-primary btn-xl">PICK ROLES</button> <button type="submit" class="btn btn-primary">PICK ROLES</button>
{% else %} {% else %}
<button type="submit" class="btn btn-confirm">OK</button> <button type="submit" class="btn btn-confirm">OK</button>
{% endif %} {% endif %}

View File

@@ -14,7 +14,7 @@
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}> <div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}"> <form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
{% csrf_token %} {% csrf_token %}
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary btn-xl">PICK<br>SIGS</button> <button id="id_pick_sigs_btn" type="submit" class="btn btn-primary">PICK<br>SIGS</button>
</form> </form>
</div> </div>
{% endif %} {% endif %}
@@ -73,7 +73,7 @@
<i class="fa-solid fa-dice-d20"></i> <i class="fa-solid fa-dice-d20"></i>
</button> </button>
</div> </div>
<div id="id_tray" style="display:none"><div id="id_tray_grid">{% for i in "12345678" %}<div class="tray-cell"></div>{% endfor %}</div></div> <div id="id_tray" style="display:none"><div id="id_tray_grid" data-role-icons-url="{% static 'apps/epic/icons/cards-roles/' %}">{% if my_tray_role %}<div class="tray-cell tray-role-card" data-role="{{ my_tray_role }}"><img src="{% static my_tray_scrawl_static_path %}" alt="{{ my_tray_role }}"></div>{% else %}<div class="tray-cell"></div>{% endif %}{% for i in "2345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
</div> </div>
{% endif %} {% endif %}
{% include "apps/gameboard/_partials/_room_gear.html" %} {% include "apps/gameboard/_partials/_room_gear.html" %}

View File

@@ -24,7 +24,7 @@
{% if navbar_recent_room_url %} {% if navbar_recent_room_url %}
<button <button
id="id_cont_game" id="id_cont_game"
class="btn btn-primary btn-xl" class="btn btn-primary"
type="button" type="button"
data-confirm="Continue game?" data-confirm="Continue game?"
data-href="{{ navbar_recent_room_url }}" data-href="{{ navbar_recent_room_url }}"