How to write "and" queries for a lookup table when you can't use the AND keyword?
A classical case of relational-division!
One of the most efficient queries techniques for what you ask:
SELECT h.name, 
       h.vacancies 
FROM   hotels_amenities_lookup ha1
JOIN   hotels_amenities_lookup ha2 USING (hotel_id)
-- more?
JOIN   hotels h ON h.id = ha1.hotel_id
WHERE  ha1.amenity_id = (SELECT id FROM amenities WHERE key = 'wheelchair_accessible')
AND    ha2.amenity_id = (SELECT id FROM amenities WHERE key = 'wifi')
-- more?

db&lt;&gt;fiddle here (reusing McNets fiddle, thanks!)
These indices help:

amenity(key, id)  -- ①
hotel_amenity(amenity_id, hotel_id)
hotel_amenity(hotel_id, amenity_id) -- ②
hotel(id) -- PK

① Maybe in the form of UNIQUE (key) INCLUDE (id). Related:

Do covering indexes in PostgreSQL help JOIN columns?

② Yes, another multi-column index with reversed columns. See:

Is a composite index also good for queries on the first field?

But there is a plethora of other ways:

How to filter SQL results in a has-many-through relation

Aside 1: The above is using AND, but it's not really about avoiding the keyword, is it?
Aside 2: Just hotels_amenities instead of hotels_amenities_lookup?
Database design
But, as Laurenz commented, the first question is about the right database design. And that heavily depends on exact (undisclosed) specifications and requirements.
Your fully normalized many-to-many implementation is a &quot;catch-all&quot; solution. You can combine as many hotels and as many amenities as you like, rename, add, drop hotels and amenities easily, add any amount of additional information in each table, and indexing with plain btree indexes is simple. Robust against heavy concurrent write load with many updates in any or all tables. But the disk footprint is high. And optimizing your query with many amenities needs some work.
For combining just a hand full of amenities the presented query is fast and optimized by the query planner automatically (using MCV lists in the column statistics). For more than a few amenities (more than join_collapse_limit) you'll have to switch to a smarter solution like outlined in the related answer you found:

Using same column multiple times in WHERE clause

The linchpin of the solution is to filter on rare amenities first.
About join_collapse_limit:

Postgres JOIN conditions vs WHERE conditions
Are implicit joins as efficient as explicit joins in Postgres?

OTOH, for a small, stable set of amenities a simple design with boolean flags like Laurenz suggested has a much smaller disk footprint. Maybe add one partial index per flag.
Or an array of amenities for more than a few options (optimized with &quot;char&quot;[]?) with a GIN index. May even be combined with your current design in the form of a MATERIALIZED VIEW.
Each with a matching query style.
If storage space is important (it always is in some way):

Should I use the PostgreSQL bit string?

As always: it depends.